В рамках работы над большим количеством android приложений появилось желание создать свой Android Market на локальном сервере (с шахматами и поэтессами).
Свой маркет должен решать две простые задачи:
  • Распространение и обновление корпоративных приложений. У нас есть набор внутри корпоративных приложений, которые нельзя публиковать на Google Play. Сейчас пользователи оповещаются о новых версиях по емайлу, что не удобно.
  • Бета тестирование заказных приложений на большой группе коллег, с обратной связью.


И так, нам необходимо реализовать небольшое Android приложение, которое будет скачивать список доступных приложений, проверять наличие обновлений для уже установленных, устанавливать/удалять приложения.

Серверная часть


Серверной частью приложения может быть как сервис с широким функционалом (регистрация пользователей, отчеты, разграничение доступа к приложениям), так и просто xml файл:
<MyMarket>
    <application name="TestApp1" 
    package="com.example.testapp1"
    versionCode="1"
    versionName="1.0" 
    url="http://mobile...../android/download/TestApp1.apk"/>
.....
</MyMarket>


В моем случае серверной частью фактически выступает http шара.
Данный xml файл в приложении трансформируется в список приложений, доступных для установки.

Установка


Пользователь выбирает нужное ему приложение и устанавливает,
Маркет скачивает apk файл на флешку устройства и инициирует установку приложения, конечно, должна быть разре��ена установка из сторонних источников.
Установить незаметно в фоне мы не можем, поэтому после выполнения данного кода:
			URL url = new URL(apkurl);
			HttpURLConnection c = (HttpURLConnection) url.openConnecвленtion();
			c.setRequestMethod("GET");
			c.setDoOutput(true);
			c.connect();

			File file = this.getExternalFilesDir("download");
			File outputFile = new File(file, "app.apk");
			FileOutputStream fos = new FileOutputStream(outputFile);
			InputStream is = c.getInputStream();

			byte[] buffer = new byte[1024];
			int len1 = 0;
			while ((len1 = is.read(buffer)) != -1) {
				fos.write(buffer, 0, len1);
			}
			fos.close();
			is.close();
			
			Intent intent = new Intent(Intent.ACTION_VIEW);
			intent.setDataAndType(Uri.fromFile(outputFile),
					"application/vnd.android.package-archive");
			startActivity(intent);

Пользователь увидит стандартное окно установки приложений:
image

Обновление


Приложение Market может по определенному интервалу проверять наличие новых версий, делается это достаточно просто, так как доступен список всех установленных приложений:
	private boolean checkNewVersion(String packageName, int versionCodeNew) {
		List<ApplicationInfo> apps = getPackageManager()
				.getInstalledApplications(0);
		for (int i = 0; i < apps.size(); i++) {
			ApplicationInfo app = apps.get(i);
			if (packageName.equals(app.packageName)) {
				PackageManager manager = getPackageManager();
				PackageInfo info;
				try {
					info = manager.getPackageInfo(app.packageName, 0);
					int versionCode = info.versionCode;
					if (versionCodeNew > versionCode) {
						Toast.makeText(this, "New Version!", Toast.LENGTH_LONG)
								.show();
						return true;
					}
				} catch (NameNotFoundException e) {
					e.printStackTrace();
				}
			}
		}
		return false;
	}


В случае необходимости обновления приложения, повторяем процесс Установки, приложение будет обновлено.

Удаление


Уд��лить программу лучше из интерфейса нашего маркета, чтобы не заставлять пользователя выискивать тестируемое приложение среди всех его программ, для этого достаточно вызвать этот код:
		Uri packageURI = Uri.parse("package:"+packageName);
		Intent intent = new Intent(Intent.ACTION_DELETE, packageURI);
		startActivity(intent);


image

Обратная связь


Получение сообщений об ошибках, вместе с логом ошибки, очень важно для разработчика, особенно на этапе тестирования приложения, добавим функцию автоматического сбора этой информации через Маркет.

Для этого в каждое приложение, распространяемое через наш Маркет добавляем следующий класс
public class CustomExceptionHandler implements UncaughtExceptionHandler {
	private File logsFolder = null;
    public static final String ERROR_INTENT = "com.example.markettestapp1.SEND_ERROR";
	public CustomExceptionHandler(File logsFolder) {
		this.logsFolder = logsFolder;
	}
	@Override
	public void uncaughtException(Thread thread, Throwable ex) {
	    final Writer result = new StringWriter();
	    final PrintWriter printWriter = new PrintWriter(result);
	    ex.printStackTrace(printWriter);
	    String stacktrace = result.toString();
	    printWriter.close();
		try {
			if (!logsFolder.exists()) {
				logsFolder.createNewFile();
			}
			BufferedWriter writer = new BufferedWriter(new FileWriter(logsFolder, true));
			writer.write(""+new Date()+"\n"+stacktrace);
			writer.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		Intent intent = new Intent();
		intent.setAction(ERROR_INTENT);
		intent.putExtra("packageName", Test1Application.getApplication().getPackageName());
		intent.putExtra("stacktrace", stacktrace);
		Test1Application.getInstanceApplication().sendBroadcast(intent);
		
		android.os.Process.killProcess(android.os.Process.myPid());
	}
}

Как видно из кода, это хендлер, который получает ошибки (Exception), генерит строку со стеком, сохраняет ее в локальный файл и отправляет ошибку в виде броадкаст сообщения.

А в Application классе включаем переадресацию всех сообщений об ошибках в этот хендлер
public class Test1Application extends Application {
	@Override
	public void onCreate() {
		super.onCreate();
		application = this;
		Thread.setDefaultUncaughtExceptionHandler(new CustomExceptionHandler(new File(this.getApplicationContext().getExternalFilesDir(null),"exceptions.log")));
	}


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

В самом Маркете мы просто отлавливаем данное сообщение:
public class SendErrorReceiver  extends BroadcastReceiver {
    public static final String ERROR_INTENT = "com.example.markettestapp1.SEND_ERROR";
    @Override
    public void onReceive(Context context, Intent intent) {
        Intent i = new Intent(context, SendErrorActivity.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        i.putExtra("stacktrace", intent.getStringExtra("stacktrace"));
        i.putExtra("packageName", intent.getStringExtra("packageName"));
        context.startActivity(i);
    }
}



AndroidManifest.xml:
        <receiver android:name="com.example.markettestapp1.SendErrorReceiver" android:enabled="true" >
            <intent-filter>
                <action android:name="com.example.markettestapp1.SEND_ERROR" >
                </action>
            </intent-filter>
        </receiver>



И дальше маркет уже может переслать это сообщение на почту разработчику, загрузить в какой нибудь веб-сервис или попросить пользователя добавить комментарий к ошибке
image

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

ps: В статье использован исходный код и картинки взятые из прототипа приложения.