Пользователь
0,0
рейтинг
20 октября 2015 в 10:55

Разработка → Простой клиент-сервер на Android (интернет-мессенджер) из песочницы tutorial

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

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



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

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

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

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



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

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

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


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

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

Таблица chat.
CREATE TABLE `chat` (
  `_id` int(11) NOT NULL AUTO_INCREMENT,
  `author` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
  `client` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
  `data` bigint(20) NOT NULL,
  `text` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`_id`)
)


Структура следующая:

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

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

$mysql_host = "localhost"; // sql сервер, может быть локальным или внешним. например mysql5.000webhost.com
$mysql_user = "l29340eb_chat"; // пользователь
$mysql_password = "123456789"; // пароль
$mysql_database = "l29340eb_chat"; // имя базы данных на сервере SQL

Файл chat.php это наш api, реализующий структуру понятных серверу запросов.
<?php // сохранить в utf-8 !
// ---------------------------------------------------------- эти значения задавались при создании БД на сервере
$mysql_host = "localhost"; // sql сервер
$mysql_user = "l29340eb_chat"; // пользователь
$mysql_password = "123456789"; // пароль
$mysql_database = "l29340eb_chat"; // имя базы данных chat

// ---------------------------------------------------------- проверяем переданные в строке запроса параметры
// например ...chat.php?action=select
//-----------------------------------------------------------
// переменная action может быть:
// select - формируем содержимое таблицы chat в JSON и отправляем назад
// insert - встваляем новую строку в таблицу chat, так же нужны 4 параметра : автор/получатель/время создания/сообщение
// ВАЖНО время создания мы не передаем в параметрах, его берем текущее на сервере
// delete - удаляет ВСЕ записи из таблицы chat - пусть будет для быстрой очистки

// ------------------------------------------- получим переданный action
if (isset($_GET["action"])) { 
    $action = $_GET['action'];
}
// ------------------------------------------- если action=insert тогда получим еще author|client|text
if (isset($_GET["author"])) { 
    $author = $_GET['author'];
}
if (isset($_GET["client"])) { 
    $client = $_GET['client'];
}
if (isset($_GET["text"])) { 
    $text = $_GET['text'];
}
// ------------------------------------------- если action=select тогда получим еще data - от после какого времени передавать ответ
if (isset($_GET["data"])) { 
    $data = $_GET['data'];
}


mysql_connect($mysql_host, $mysql_user, $mysql_password); // коннект к серверу SQL
mysql_select_db($mysql_database); // коннект к БД на сервере
mysql_set_charset('utf8'); // кодировка
// ------------------------------------------------------------ обрабатываем запрос если он был
if($action == select){ // если действие SELECT

if($data == null){
// выберем из таблицы chat ВСЕ данные что есть и вернем их в JSON
$q=mysql_query("SELECT * FROM chat");


}else{
	
// выберем из таблицы chat ВСЕ данные ПОЗНЕЕ ОПРЕДЕЛЕННОГО ВРЕМЕНИ и вернем их в JSON
$q=mysql_query("SELECT * FROM chat WHERE data > $data");	
	
}
while($e=mysql_fetch_assoc($q))
        $output[]=$e;
print(json_encode($output));

}


if($action == insert && $author != null && $client != null && $text != null){ // если действие INSERT и есть все что нужно

// время = время сервера а не клиента !
$current_time = round(microtime(1) * 1000);
// пример передачи скрипту данных:
// chat.php?action=insert&author=author&client=client&text=text
// вставим строку с переданными параметрами
mysql_query("INSERT INTO `chat`(`author`,`client`,`data`,`text`) VALUES ('$author','$client','$current_time','$text')");

}


if($action == delete){ // если действие DELETE
// полностью обнулим таблицу записей
mysql_query("TRUNCATE TABLE `chat`");	
}

mysql_close();
?>


Структура запросов к 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 формате

Файл showBD.php — не обязательный скрипт для наглядного отображения содержимого БД в браузере.
<?php // сохранить utf-8 !
// -------------------------------------------------------------------------- логины пароли
$mysql_host = "localhost"; // sql сервер
$mysql_user = "l29340eb_chat"; // пользователь
$mysql_password = "123456789"; // пароль
$mysql_database = "l29340eb_chat"; // имя базы данных chat
// -------------------------------------------------------------------------- если база недоступна
if (!mysql_connect($mysql_host, $mysql_user, $mysql_password)){
	
echo "<h2>База недоступна!</h2>";
exit;
}else{
// -------------------------------------------------------------------------- если база доступна
echo "<h2>База доступна!</h2>";


mysql_select_db($mysql_database);
mysql_set_charset('utf8');
// -------------------------------------------------------------------------- выведем JSON
$q=mysql_query("SELECT * FROM chat");
echo "<h3>Json ответ:</h3>";
// Выводим json
while($e=mysql_fetch_assoc($q))
        $output[]=$e;
print(json_encode($output));

// -------------------------------------------------------------------------- выведем таблицу
$q=mysql_query("SELECT * FROM chat");
echo "<h3>Табличный вид:</h3>";

echo "<table border=\"1\" width=\"100%\" bgcolor=\"#999999\">";
echo "<tr><td>_id</td><td>author</td>";
echo "<td>client</td><td>data</td><td>text</td></tr>";

for ($c=0; $c<mysql_num_rows($q); $c++){

$f = mysql_fetch_array($q);
echo "<tr><td>$f[_id]</td><td>$f[author]</td><td>$f[client]</td><td>$f[data]</td><td>$f[text]</td></tr>";

}
 echo "</tr></table>";

}
mysql_close();
// -------------------------------------------------------------------------- разорвем соединение с БД
?>


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


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



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

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

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

В MainActivity.java, для наглядности, я добавил возможность удаления сообщений из локальной БД — это эквивалентно чистой установке приложения (в этом случае FoneService отправит на сервер запрос на получение всех сообщений выбранного чата). Так же есть возможность послать запрос на удаление всех сообщений из БД, расположенной на сервере.

Код активностей:

FoneService.java
package by.andreidanilevich.temp_chat;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

import org.json.JSONArray;
import org.json.JSONObject;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.BitmapFactory;
import android.os.IBinder;
import android.util.Log;

public class FoneService extends Service {

	// ИМЯ СЕРВЕРА (url зарегистрированного нами сайта)
	// например http://l29340eb.bget.ru
	String server_name = "http://l29340eb.bget.ru";
	
	SQLiteDatabase chatDBlocal;
	HttpURLConnection conn;
	Cursor cursor;
	Thread thr;
	ContentValues new_mess;
	Long last_time; // время последней записи в БД, отсекаем по нему что нам
					// тянуть с сервера, а что уже есть

	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}

	public void onStart(Intent intent, int startId) {

		Log.i("chat", "+ FoneService - запуск сервиса");

		chatDBlocal = openOrCreateDatabase("chatDBlocal.db",
				Context.MODE_PRIVATE, null);
		chatDBlocal
				.execSQL("CREATE TABLE IF NOT EXISTS chat (_id integer primary key autoincrement, author, client, data, text)");

		// создадим и покажем notification
		// это позволит стать сервису "бессмертным"
		// и будет визуально видно в трее
		Intent iN = new Intent(getApplicationContext(), MainActivity.class);
		intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
				| Intent.FLAG_ACTIVITY_SINGLE_TOP);
		PendingIntent pI = PendingIntent.getActivity(getApplicationContext(),
				0, iN, PendingIntent.FLAG_CANCEL_CURRENT);
		Notification.Builder bI = new Notification.Builder(
				getApplicationContext());

		bI.setContentIntent(pI)
				.setSmallIcon(R.drawable.ic_launcher)
				.setLargeIcon(
						BitmapFactory.decodeResource(getApplicationContext()
								.getResources(), R.drawable.ic_launcher))
				.setAutoCancel(true)
				.setContentTitle(getResources().getString(R.string.app_name))
				.setContentText("работаю...");

		Notification notification = bI.build();
		startForeground(101, notification);

		startLoop();
	}

	// запуск потока, внутри которого будет происходить
	// регулярное соединение с сервером для чтения новых
	// сообщений.
	// если сообщения найдены - отправим броадкаст для обновления
	// ListView в ChatActivity
	private void startLoop() {

		thr = new Thread(new Runnable() {

			// ansver = ответ на запрос
			// lnk = линк с параметрами
			String ansver, lnk;

			public void run() {

				while (true) { // стартуем бесконечный цикл

					// глянем локальную БД на наличие сообщщений чата
					cursor = chatDBlocal.rawQuery(
							"SELECT * FROM chat ORDER BY data", null);

					// если какие-либо сообщения есть - формируем запрос
					// по которому получим только новые сообщения
					if (cursor.moveToLast()) {
						last_time = cursor.getLong(cursor
								.getColumnIndex("data"));
						lnk = server_name + "/chat.php?action=select&data="
								+ last_time.toString();

						// если сообщений в БД нет - формируем запрос
						// по которому получим всё
					} else {
						lnk = server_name + "/chat.php?action=select";
					}

					cursor.close();

					// создаем соединение ---------------------------------->
					try {
						Log.i("chat",
								"+ FoneService --------------- ОТКРОЕМ СОЕДИНЕНИЕ");

						conn = (HttpURLConnection) new URL(lnk)
								.openConnection();
						conn.setReadTimeout(10000);
						conn.setConnectTimeout(15000);
						conn.setRequestMethod("POST");
						conn.setRequestProperty("User-Agent", "Mozilla/5.0");
						conn.setDoInput(true);
						conn.connect();

					} catch (Exception e) {
						Log.i("chat", "+ FoneService ошибка: " + e.getMessage());
					}
					// получаем ответ ---------------------------------->
					try {
						InputStream is = conn.getInputStream();
						BufferedReader br = new BufferedReader(
								new InputStreamReader(is, "UTF-8"));
						StringBuilder sb = new StringBuilder();
						String bfr_st = null;
						while ((bfr_st = br.readLine()) != null) {
							sb.append(bfr_st);
						}

						Log.i("chat", "+ FoneService - полный ответ сервера:\n"
								+ sb.toString());
						// сформируем ответ сервера в string
						// обрежем в полученном ответе все, что находится за "]"
						// это необходимо, т.к. json ответ приходит с мусором
						// и если этот мусор не убрать - будет невалидным
						ansver = sb.toString();
						ansver = ansver.substring(0, ansver.indexOf("]") + 1);

						is.close(); // закроем поток
						br.close(); // закроем буфер

					} catch (Exception e) {
						Log.i("chat", "+ FoneService ошибка: " + e.getMessage());
					} finally {
						conn.disconnect();
						Log.i("chat",
								"+ FoneService --------------- ЗАКРОЕМ СОЕДИНЕНИЕ");
					}

					// запишем ответ в БД ---------------------------------->
					if (ansver != null && !ansver.trim().equals("")) {

						Log.i("chat",
								"+ FoneService ---------- ответ содержит JSON:");

						try {
							// ответ превратим в JSON массив
							JSONArray ja = new JSONArray(ansver);
							JSONObject jo;

							Integer i = 0;

							while (i < ja.length()) {

								// разберем JSON массив построчно
								jo = ja.getJSONObject(i);

								Log.i("chat",
										"=================>>> "
												+ jo.getString("author")
												+ " | "
												+ jo.getString("client")
												+ " | " + jo.getLong("data")
												+ " | " + jo.getString("text"));

								// создадим новое сообщение
								new_mess = new ContentValues();
								new_mess.put("author", jo.getString("author"));
								new_mess.put("client", jo.getString("client"));
								new_mess.put("data", jo.getLong("data"));
								new_mess.put("text", jo.getString("text"));
								// запишем новое сообщение в БД
								chatDBlocal.insert("chat", null, new_mess);
								new_mess.clear();

								i++;

								// отправим броадкаст для ChatActivity
								// если она открыта - она обновить ListView
								sendBroadcast(new Intent(
										"by.andreidanilevich.action.UPDATE_ListView"));
							}
						} catch (Exception e) {
							// если ответ сервера не содержит валидный JSON
							Log.i("chat",
									"+ FoneService ---------- ошибка ответа сервера:\n"
											+ e.getMessage());
						}
					} else {
						// если ответ сервера пустой
						Log.i("chat",
								"+ FoneService ---------- ответ не содержит JSON!");
					}

					try {
						Thread.sleep(15000);
					} catch (Exception e) {
						Log.i("chat",
								"+ FoneService - ошибка процесса: "
										+ e.getMessage());
					}
				}
			}
		});

		thr.setDaemon(true);
		thr.start();

	}
}


ChatActivity.java
package by.andreidanilevich.temp_chat;

import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.Toast;

public class ChatActivity extends Activity {

	// ИМЯ СЕРВЕРА (url зарегистрированного нами сайта)
	// например http://l29340eb.bget.ru
	String server_name = "http://l29340eb.bget.ru";

	ListView lv; // полоса сообщений
	EditText et;
	Button bt;
	SQLiteDatabase chatDBlocal;
	String author, client;
	INSERTtoChat insert_to_chat; // класс отправляет новое сообщение на сервер
	UpdateReceiver upd_res; // класс ждет сообщение от сервиса и получив его -
							// обновляет ListView

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.chat);

		// получим 2 переменные по которым будем отбирать инфу из БД:
		// author - от чьего имени идет чат
		// client - с кем чатимся
		Intent intent = getIntent();
		author = intent.getStringExtra("author");
		client = intent.getStringExtra("client");

		Log.i("chat", "+ ChatActivity - открыт author = " + author
				+ " | client = " + client);

		lv = (ListView) findViewById(R.id.lv);
		et = (EditText) findViewById(R.id.et);
		bt = (Button) findViewById(R.id.bt);

		chatDBlocal = openOrCreateDatabase("chatDBlocal.db",
				Context.MODE_PRIVATE, null);
		chatDBlocal
				.execSQL("CREATE TABLE IF NOT EXISTS chat (_id integer primary key autoincrement, author, client, data, text)");

		// Создаём и регистрируем широковещательный приёмник

		upd_res = new UpdateReceiver();
		registerReceiver(upd_res, new IntentFilter(
				"by.andreidanilevich.action.UPDATE_ListView"));

		create_lv();
	}

	// обновим lv = заполним нужные позиции в lv информацией из БД
	@SuppressLint("SimpleDateFormat")
	public void create_lv() {

		Cursor cursor = chatDBlocal.rawQuery(
				"SELECT * FROM chat WHERE author = '" + author
						+ "' OR author = '" + client + "' ORDER BY data", null);
		if (cursor.moveToFirst()) {
			// если в базе есть элементы соответствующие
			// нашим критериям отбора

			// создадим массив, создадим hashmap и заполним его результатом
			// cursor
			ArrayList<HashMap<String, Object>> mList = new ArrayList<HashMap<String, Object>>();
			HashMap<String, Object> hm;

			do {
				// мое сообщение !!!
				// если автор сообщения = автор
				// и получатель сообщения = клиент
				if (cursor.getString(cursor.getColumnIndex("author")).equals(
						author)
						&& cursor.getString(cursor.getColumnIndex("client"))
								.equals(client)) {

					hm = new HashMap<>();
					hm.put("author", author);
					hm.put("client", "");
					hm.put("list_client", "");
					hm.put("list_client_time", "");
					hm.put("list_author",
							cursor.getString(cursor.getColumnIndex("text")));
					hm.put("list_author_time", new SimpleDateFormat(
							"HH:mm - dd.MM.yyyy").format(new Date(cursor
							.getLong(cursor.getColumnIndex("data")))));
					mList.add(hm);

				}

				// сообщение мне !!!!!!!
				// если автор сообщения = клиент
				// и если получатель сообщения = автор
				if (cursor.getString(cursor.getColumnIndex("author")).equals(
						client)
						&& cursor.getString(cursor.getColumnIndex("client"))
								.equals(author)) {

					hm = new HashMap<>();
					hm.put("author", "");
					hm.put("client", client);
					hm.put("list_author", "");
					hm.put("list_author_time", "");
					hm.put("list_client",
							cursor.getString(cursor.getColumnIndex("text")));
					hm.put("list_client_time", new SimpleDateFormat(
							"HH:mm - dd.MM.yyyy").format(new Date(cursor
							.getLong(cursor.getColumnIndex("data")))));
					mList.add(hm);

				}

			} while (cursor.moveToNext());

			// покажем lv
			SimpleAdapter adapter = new SimpleAdapter(getApplicationContext(),
					mList, R.layout.list, new String[] { "list_author",
							"list_author_time", "list_client",
							"list_client_time", "author", "client" },
					new int[] { R.id.list_author, R.id.list_author_time,
							R.id.list_client, R.id.list_client_time,
							R.id.author, R.id.client });

			lv.setAdapter(adapter);
			cursor.close();

		}

		Log.i("chat",
				"+ ChatActivity ======================== обновили поле чата");

	}

	public void send(View v) {
		// запишем наше новое сообщение
		// вначале проверим на пустоту

		if (!et.getText().toString().trim().equals("")) {

			// кнопку сделаем неактивной
			bt.setEnabled(false);

			// если чтото есть - действуем!
			insert_to_chat = new INSERTtoChat();
			insert_to_chat.execute();

		} else {
			// если ничего нет - нечего и писать
			et.setText("");
		}
	}

	// отправим сообщение на сервер

	private class INSERTtoChat extends AsyncTask<Void, Void, Integer> {

		HttpURLConnection conn;
		Integer res;

		protected Integer doInBackground(Void... params) {

			try {

				// соберем линк для передачи новой строки
				String post_url = server_name
						+ "/chat.php?action=insert&author="
						+ URLEncoder.encode(author, "UTF-8")
						+ "&client="
						+ URLEncoder.encode(client, "UTF-8")
						+ "&text="
						+ URLEncoder.encode(et.getText().toString().trim(),
								"UTF-8");

				Log.i("chat",
						"+ ChatActivity - отправляем на сервер новое сообщение: "
								+ et.getText().toString().trim());

				URL url = new URL(post_url);
				conn = (HttpURLConnection) url.openConnection();
				conn.setConnectTimeout(10000); // ждем 10сек
				conn.setRequestMethod("POST");
				conn.setRequestProperty("User-Agent", "Mozilla/5.0");
				conn.connect();

				res = conn.getResponseCode();
				Log.i("chat", "+ ChatActivity - ответ сервера (200 - все ОК): "
						+ res.toString());

			} catch (Exception e) {
				Log.i("chat",
						"+ ChatActivity - ошибка соединения: " + e.getMessage());

			} finally {
				// закроем соединение
				conn.disconnect();
			}
			return res;
		}

		protected void onPostExecute(Integer result) {

			try {
				if (result == 200) {
					Log.i("chat", "+ ChatActivity - сообщение успешно ушло.");
					// сбросим набранный текст
					et.setText("");
				}
			} catch (Exception e) {
				Log.i("chat", "+ ChatActivity - ошибка передачи сообщения:\n"
						+ e.getMessage());
				Toast.makeText(getApplicationContext(),
						"ошибка передачи сообщения", Toast.LENGTH_SHORT).show();
			} finally {
				// активируем кнопку
				bt.setEnabled(true);
			}
		}
	}

	// ресивер приёмник ждет сообщения от FoneService
	// если сообщение пришло, значит есть новая запись в БД - обновим ListView
	public class UpdateReceiver extends BroadcastReceiver {
		@Override
		public void onReceive(Context context, Intent intent) {
			Log.i("chat",
					"+ ChatActivity - ресивер получил сообщение - обновим ListView");
			create_lv();
		}
	}

	// выходим из чата
	public void onBackPressed() {
		Log.i("chat", "+ ChatActivity - закрыт");
		unregisterReceiver(upd_res);
		finish();
	}
}


MainActivity.java
package by.andreidanilevich.temp_chat;

import java.net.HttpURLConnection;
import java.net.URL;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.Toast;

public class MainActivity extends Activity {

	// ИМЯ СЕРВЕРА (url зарегистрированного нами сайта)
	// например http://l29340eb.bget.ru
	String server_name = "http://l29340eb.bget.ru";

	Spinner spinner_author, spinner_client;
	String author, client;
	Button open_chat_btn, open_chat_reverce_btn, delete_server_chat;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		Log.i("chat", "+ MainActivity - запуск приложения");

		open_chat_btn = (Button) findViewById(R.id.open_chat_btn);
		open_chat_reverce_btn = (Button) findViewById(R.id.open_chat_reverce_btn);
		delete_server_chat = (Button) findViewById(R.id.delete_server_chat);

		// запустим FoneService
		this.startService(new Intent(this, FoneService.class));

		// заполним 2 выпадающих меню для выбора автора и получателя сообщения
		// 5 мужских и 5 женских имен
		// установим слушателей
		spinner_author = (Spinner) findViewById(R.id.spinner_author);
		spinner_client = (Spinner) findViewById(R.id.spinner_client);

		spinner_author.setAdapter(new ArrayAdapter<String>(this,
				android.R.layout.simple_spinner_item, new String[] { "Петя",
						"Вася", "Коля", "Андрей", "Сергей", "Оля", "Лена",
						"Света", "Марина", "Наташа" }));
		spinner_client.setAdapter(new ArrayAdapter<String>(this,
				android.R.layout.simple_spinner_item, new String[] { "Петя",
						"Вася", "Коля", "Андрей", "Сергей", "Оля", "Лена",
						"Света", "Марина", "Наташа" }));
		spinner_client.setSelection(5);

		open_chat_btn.setText("Открыть чат: "
				+ spinner_author.getSelectedItem().toString() + " > "
				+ spinner_client.getSelectedItem().toString());
		open_chat_reverce_btn.setText("Открыть чат: "
				+ spinner_client.getSelectedItem().toString() + " > "
				+ spinner_author.getSelectedItem().toString());

		spinner_author
				.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
					public void onItemSelected(AdapterView<?> parent,
							View itemSelected, int selectedItemPosition,
							long selectedId) {

						author = spinner_author.getSelectedItem().toString();

						open_chat_btn.setText("Открыть чат: "
								+ spinner_author.getSelectedItem().toString()
								+ " > "
								+ spinner_client.getSelectedItem().toString());
						open_chat_reverce_btn.setText("Открыть чат: "
								+ spinner_client.getSelectedItem().toString()
								+ " > "
								+ spinner_author.getSelectedItem().toString());
					}

					public void onNothingSelected(AdapterView<?> parent) {
					}
				});

		spinner_client
				.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
					public void onItemSelected(AdapterView<?> parent,
							View itemSelected, int selectedItemPosition,
							long selectedId) {

						client = spinner_client.getSelectedItem().toString();

						open_chat_btn.setText("Открыть чат: "
								+ spinner_author.getSelectedItem().toString()
								+ " > "
								+ spinner_client.getSelectedItem().toString());
						open_chat_reverce_btn.setText("Открыть чат: "
								+ spinner_client.getSelectedItem().toString()
								+ " > "
								+ spinner_author.getSelectedItem().toString());
					}

					public void onNothingSelected(AdapterView<?> parent) {
					}
				});
	}

	// откроем чат с выбранным автором и получателем
	public void open_chat(View v) {
		// быстрая проверка
		if (author.equals(client)) {
			// если автор и получатель одинаковы
			// чат не открываем
			Toast.makeText(this, "author = client !", Toast.LENGTH_SHORT)
					.show();
		} else {
			// откроем нужный чат author > client
			Intent intent = new Intent(MainActivity.this, ChatActivity.class);
			intent.putExtra("author", author);
			intent.putExtra("client", client);
			startActivity(intent);
		}
	}

	// откроем чат с выбранным автором и получателем, только наоборот
	public void open_chat_reverce(View v) {
		// быстрая проверка
		if (author.equals(client)) {
			// если автор и получатель одинаковы
			// чат не открываем
			Toast.makeText(this, "author = client !", Toast.LENGTH_SHORT)
					.show();
		} else {
			// откроем нужный чат client > author
			Intent intent = new Intent(MainActivity.this, ChatActivity.class);
			intent.putExtra("author", client);
			intent.putExtra("client", author);
			startActivity(intent);
		}
	}

	// отправим запрос на сервер о удалении таблицы с чатами
	public void delete_server_chats(View v) {

		Log.i("chat", "+ MainActivity - запрос на удаление чата с сервера");

		delete_server_chat.setEnabled(false);
		delete_server_chat.setText("Запрос отправлен. Ожидайте...");

		DELETEfromChat delete_from_chat = new DELETEfromChat();
		delete_from_chat.execute();
	}

	// удалим локальную таблицу чатов
	// и создадим такуюже новую
	public void delete_local_chats(View v) {

		Log.i("chat", "+ MainActivity - удаление чата с этого устройства");

		SQLiteDatabase chatDBlocal;
		chatDBlocal = openOrCreateDatabase("chatDBlocal.db",
				Context.MODE_PRIVATE, null);
		chatDBlocal.execSQL("drop table chat");
		chatDBlocal
				.execSQL("CREATE TABLE IF NOT EXISTS chat (_id integer primary key autoincrement, author, client, data, text)");

		Toast.makeText(getApplicationContext(),
				"Чат на этом устройстве удален!", Toast.LENGTH_SHORT).show();
	}

	// отправим запрос на сервер о удалении таблицы с чатами
	// если он пройдет - таблица будет удалена
	// если не пройдет (например нет интернета или сервер недоступен)
	// - покажет сообщение
	private class DELETEfromChat extends AsyncTask<Void, Void, Integer> {

		Integer res;
		HttpURLConnection conn;

		protected Integer doInBackground(Void... params) {

			try {
				URL url = new URL(server_name + "/chat.php?action=delete");
				conn = (HttpURLConnection) url.openConnection();
				conn.setConnectTimeout(10000); // ждем 10сек
				conn.setRequestMethod("POST");
				conn.setRequestProperty("User-Agent", "Mozilla/5.0");
				conn.connect();
				res = conn.getResponseCode();
				Log.i("chat", "+ MainActivity - ответ сервера (200 = ОК): "
						+ res.toString());

			} catch (Exception e) {
				Log.i("chat",
						"+ MainActivity - ответ сервера ОШИБКА: "
								+ e.getMessage());
			} finally {
				conn.disconnect();
			}

			return res;
		}

		protected void onPostExecute(Integer result) {

			try {
				if (result == 200) {
					Toast.makeText(getApplicationContext(),
							"Чат на сервере удален!", Toast.LENGTH_SHORT)
							.show();
				}
			} catch (Exception e) {
				Toast.makeText(getApplicationContext(),
						"Ошибка выполнения запроса.", Toast.LENGTH_SHORT)
						.show();
			} finally {
				// сделаем кнопку активной
				delete_server_chat.setEnabled(true);
				delete_server_chat.setText("Удалить все чаты на сервере!");
			}
		}
	}

	public void onBackPressed() {
		Log.i("chat", "+ MainActivity - выход из приложения");
		finish();
	}
}


AutoRun.java
package by.andreidanilevich.temp_chat;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class AutoRun extends BroadcastReceiver {

	@Override
	public void onReceive(Context context, Intent intent) {
		if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {

			// получили boot_completed - запустили FoneService
			context.startService(new Intent(context, FoneService.class));
			Log.i("chat", "+ AutoRun - отработал");
		}
	}
}
<b>Манифест:</b>


AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="by.andreidanilevich.temp_chat"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <uses-sdk
        android:minSdkVersion="16"
        android:targetSdkVersion="22" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".ChatActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait" >
        </activity>

        <receiver
            android:name=".AutoRun"
            android:enabled="true"
            android:exported="false" >
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

        <service android:name=".FoneService" />
    </application>

</manifest>


Разметка:

Разметка chat.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#999999" >

    <ListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="60dp"
        android:scrollbars="none"
        android:stackFromBottom="true"
        android:transcriptMode="alwaysScroll" >
    </ListView>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_alignParentBottom="true"
        android:background="#ffffff" >

        <EditText
            android:id="@+id/et"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:layout_weight="1"
            android:ems="10" >
        </EditText>

        <Button
            android:id="@+id/bt"
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:onClick="send"
            android:text=">" />
    </LinearLayout>

</RelativeLayout>


Разметка main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="${relativePackage}.${activityClass}" >

    <Spinner
        android:id="@+id/spinner_author"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Spinner
        android:id="@+id/spinner_client"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/open_chat_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="open_chat" />

    <Button
        android:id="@+id/open_chat_reverce_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="open_chat_reverce" />

    <Button
        android:id="@+id/delete_server_chat"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:onClick="delete_server_chats"
        android:text="Удалить все чаты на сервере!" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="delete_local_chats"
        android:text="Удалить все чаты на этом устройстве!" />

</LinearLayout>


Разметка list.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#999999"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp" >

        <TextView
            android:id="@+id/author"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:textColor="#c6c6c6"
            android:textSize="12sp" />

        <TextView
            android:id="@+id/client"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:textColor="#c6c6c6"
            android:textSize="12sp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:layout_marginTop="5dp" >

        <TextView
            android:id="@+id/list_author"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:textColor="#0000ff"
            android:textSize="14sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/list_client"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:textColor="#ffff00"
            android:textSize="14sp"
            android:textStyle="bold" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp" >

        <TextView
            android:id="@+id/list_author_time"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="right"
            android:textColor="#c6c6c6"
            android:textSize="12sp" />

        <TextView
            android:id="@+id/list_client_time"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="right"
            android:textColor="#c6c6c6"
            android:textSize="12sp" />
    </LinearLayout>

</LinearLayout>


И все вместе (+ apk):

github.com/andreidanilevich/temp_chat

P.S.:

1. Если Вы загрузили apk по ссылке выше, вначале проверьте доступность моего «сервера» по адресу l29340eb.bget.ru/showBD.php.
2. Код писался давно, конечно не все красиво и по канонам, возможно не все исключения обработаны и есть ошибки. Это черновик. У меня все работало на реальных устройствах и на эмуляторе.
3. Если кому-либо пригодится — буду рад. За критику — спасибо, за конструктивные сообщения — спасибо вдвойне. На вопросы постараюсь ответить.
@andreidanilevich
карма
1,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (10)

  • +5
    Чтобы не делать каждые 15 секунд запрос на сервер придумали push notification. Ваше приложение в лучшем случае будет просто прибито системой, в худшем не даст перейти смартфону в спящий режим и будет сильно кушать батарейку.
    • 0
      а что насчёт longpool?
      • 0
        longpoll
        • 0
          да, точно
          поправить уже не смогу
    • +1
      Android OS, по идее, как раз оставит жить такое приложение и не будет его прибивать. Логика у OS, вроде бы, такая. Есть живущий сервис -> приложение делает какую-то полезную работу -> не трогаем его пока не возникнет критическая нехватка ресурсов.
  • +4
    Какой ужасный код. Что на Java, что PHP.
    Я бы вас на работу не взял.
  • +1
    FoneService.java

    Не делайте production версию своего мессенджера, пожалуйста. Им могут пользоваться не виноватые люди.
  • +2
    Умиляет, когда camelCase во всех стандартных методах и аргументах людям ни на что не намекает.
  • 0
    1. Каждые 15 секунд ходить на сервер — это сказать пока-пока всему заряду батарейки
    2. Уже не надо для этого писать свои сервисы, уже давно стоит использовать проверенные библиотеки. Они решат всю проблему с транспортом, оставят вам разбираться только с собственной логикой.
  • 0
    ))
    1. Очевидно что для реального приложения лучше push. В моем примере его использование увеличило бы количество букв в 2 раза. Да и изначально, не было цели делать «чат». Была мысль быстро написать пример клиент(android)-сервер(php). Уже в процессе понял что «чат» будет нагляднее.
    2. Android «прибьет» этот сервис в последнюю очередь не потому, что он «делает какую то работу», а благодаря startForeground(101, notification);
    3. Ужасный код… ну да, чего уж ))) на php это вообще первые строчки т.с.
    4. «Не делайте production версию своего мессенджера, пожалуйста. Им могут пользоваться не виноватые люди.»
    — ок ))))))))))))) обещаю!
    5. каждые 15 сек. на сервер… ну это же для примера! Впрочем именно в этом примере батарейке ничего не грозит )). Пока смартфон активен — приложение «ходит на сервер», как только уснул — приложение уснуло вместе с ним. PowerManager как видите я нигде не использовал.

    Спасибо! ))

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.