Как стать автором
Обновить

Реализация приложения – device owner-а под Android

Время на прочтение7 мин
Количество просмотров16K
Эта статья представляет собой инструкцию по написанию и установке на целевое устройство приложения – device owner-а. Меня побудило написать эту статью то, что когда я сам принялся изучать этот вопрос, оказалось, что хорошей официальной документации с примерами нет, а информацию пришлось собирать с помощью Гугла.

В ОС Android, начиная с версии 5.0 Lollipop (API 21) появилась замечательная возможность управлять устройством программно, находясь в режиме device owner. Например, стало возможно производить «тихую» установку/удаление приложений, «скрывать» приложения (причем скрываются они качественно, т.е. исчезают из списка приложений в настройках, исчезают из лаунчера и списка последних использованных приложений), и делать многое другое. Это очень полезные возможности для реализации MDM. Обзор всех возможностей, которые предоставляются device owner-у выходят за рамки статьи, о них можно почитать здесь и здесь.

Терминология


Для начала определимся с терминами. Device owner – очевидно владелец устройства. Обычно владельцем устройства становится первый созданный пользователь при начальной настройке устройства, которая происходит после того как устройство впервые включается после покупки. Но в нашем случае мы сделаем так, что владельцем устройства будет наше приложение, которое и получит доступ к использованию расширенных возможностей управления устройством. Unprovisioned state – состояние устройства до того как будет проведена первичная настройка. Устройство находится в этом состоянии после первого включения и после wipe. Provisioned state – состояние устройства после того, как была проведена первичная настройка.

Что потребуется?


Для наших экспериментов нам понадобятся два устройства на которых есть NFC и одно из этих устройств нам придется wipe-нуть. Для того чтобы установить приложение – device owner на целевое устройство нужно загрузить его на сервер так, чтобы оно было доступно по URL целевому устройству, например example.com/deviceowner.apk (я пробовал только протокол http). Далее нужно привести целевое устройство в unprovisioned state, например сделать wipe. После этого нужно установить на другое устройство приложение – инсталлятор. Потом нужно совместить эти два устройства так, чтобы был возможен обмен данными по NFC, обычно надо просто приложить устройства друг к другу задними поверхностями, далее надо подтвердить передачу по NFC тапнув по экрану устройства с приложением – инсталлятором. После этого первичная настройка целевого устройства продолжится и потребуется настроить сеть так, чтобы устройство смогло скачать apk файл с приложением – device owner-ом. После завершения первичной настройки приложение – device owner будет доступно для запуска и использования и его невозможно будет остановить/удалить никак и ничем, кроме wipe-а устройства.

Приложение – device owner


Я написал пример приложений device owner и инсталлятора. Ниже приведу наиболее интересные фрагменты кода. Весь код писать в статье смысла нет, в конце статьи будут ссылки на проекты с полным исходным кодом.

Приложение – device owner будет управлять видимостью других приложений. Т.е. с его помощью можно скрывать/показывать другие приложения. В приложенит есть класс AppsManager, он инкапсулирует построение списка и управление приложениями. Список получается в AsyncTaske-е:

private class LoadingTask extends AsyncTask<Void, Void, List<ApplicationInfo>> {
	@Override
	protected List<ApplicationInfo> doInBackground(final Void... params) {
		final PackageManager packageManager = mContext.getPackageManager();
		return packageManager.getInstalledApplications(
				PackageManager.GET_META_DATA | PackageManager.GET_UNINSTALLED_PACKAGES
		);
	}

	@Override
	protected void onPostExecute(final List<ApplicationInfo> result) {
		if (result != null) {
			mAppsList = result;
		} else {
			mAppsList = Lists.newArrayList();
		}

		mStateObservable.setValue(State.IDLE);
	}
}

Управление происходит через DevicePolicyManager:

mDevicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);

public void showApp(final ApplicationInfo app) {
	if (mStateObservable.getValue() != State.IDLE) {
		return;
	}

	mDevicePolicyManager.setApplicationHidden(mAdminComponent, app.packageName, false);
}

public void hideApp(final ApplicationInfo app) {
	if (mStateObservable.getValue() != State.IDLE) {
		return;
	}

	mDevicePolicyManager.setApplicationHidden(mAdminComponent, app.packageName, true);
}

public boolean isAppHidden(final ApplicationInfo app) {
	return mDevicePolicyManager.isApplicationHidden(mAdminComponent, app.packageName);
}

Основой UI служит RecyclerView, все тривиально, приведу код адаптера:

private static class AppsListAdapter extends RecyclerView.Adapter<AppsListAdapter.ViewHolder> {
	private final Context mContext;
	private final LayoutInflater mInflater;
	private final PackageManager mPackageManager;
	private List<ApplicationInfo> mAppsList;
	private final AdministrationModeManager mAdministrationModeManager = AdministrationModeManager.getInstance();
	private final AppsManager mAppsManager = AppsManager.getInstance();

	public AppsListAdapter(final Context context) {
		mContext = context;
		mInflater = LayoutInflater.from(mContext);
		mPackageManager = mContext.getPackageManager();
	}

	@Override
	public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
		final View layout = mInflater.inflate(android.R.layout.simple_list_item_multiple_choice, parent, false);
		return new ViewHolder(layout);
	}

	@Override
	public void onBindViewHolder(final ViewHolder holder, final int position) {
		holder.mAppTitleTextView.setText(mAppsList.get(position).loadLabel(mPackageManager));
		if (mAdministrationModeManager.isAdministrator() && mAdministrationModeManager.isDeviceOwner()) {
			holder.mAppTitleTextView.setChecked(!mAppsManager.isAppHidden(mAppsList.get(position)));
		}
	}

	@Override
	public int getItemCount() {
		return mAppsList == null ? 0 : mAppsList.size();
	}

	public void setAppsList(final List<ApplicationInfo> appsList) {
		mAppsList = appsList;
	}

	public class ViewHolder extends RecyclerView.ViewHolder {
		public final CheckedTextView mAppTitleTextView;

		public ViewHolder(final View itemView) {
			super(itemView);

			mAppTitleTextView = (CheckedTextView) itemView.findViewById(android.R.id.text1);
			mAppTitleTextView.setOnClickListener(new View.OnClickListener() {
				@Override
				public void onClick(final View v) {
					if (mAdministrationModeManager.isAdministrator() &&
							mAdministrationModeManager.isDeviceOwner()) {
						if (mAppTitleTextView.isChecked()) {
							mAppsManager.hideApp(mAppsList.get(getAdapterPosition()));
						} else {
							mAppsManager.showApp(mAppsList.get(getAdapterPosition()));
						}
						notifyDataSetChanged();
					}
				}
			});
		}
	}
}

Особенность приложения в том, что ему нужно реализовать какой-нибудь reciever чтобы получить права администратора, хотя бы пустой:

public class AdminReceiver extends DeviceAdminReceiver {
	// do nothing
}

В манифесте нужно указать этот receiver с соответствующими настройками:

<receiver
		android:name=".AdminReceiver"
		android:description="@string/app_name"
		android:label="@string/app_name"
		android:permission="android.permission.BIND_DEVICE_ADMIN">

	<meta-data
			android:name="android.app.device_admin"
			android:resource="@xml/device_owner_receiver"/>

	<intent-filter>

		<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
		<action android:name="android.app.action.PROFILE_PROVISIONING_COMPLETE"/>

	</intent-filter>

</receiver>

Для того, чтобы эти настройки заработали надо также положить в директорию ресурсов «xml» файл device_owner_receiver.xml с описанием того, чем приложение собирается управлять как администратор:

<?xml version="1.0" encoding="utf-8"?>
<device-admin>
	<uses-policies>
		<limit-password/>
		<watch-login/>
		<reset-password/>
		<force-lock/>
		<wipe-data/>
		<expire-password/>
		<encrypted-storage/>
		<disable-camera/>
	</uses-policies>
</device-admin>

В итоге приложение надо собрать и apk файл выложить на сервер.

Приложение – инсталлятор


Приложение – инсталлятор это просто приложение, которое запускается и после совмещения с целевым устройством передает по NFC данные в которых содержится информация откуда целевое устройство должно брать приложение – device owner, ниже код формирующий NFC-сообщение:

private class NdefMessageCallback implements NfcAdapter.CreateNdefMessageCallback {
	@Override
	public NdefMessage createNdefMessage(final NfcEvent event) {
		final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		final Properties properties = new Properties();

		properties.put(DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME, "com.example.deviceowner");

		// Make sure to put local time in the properties. This is necessary on some devices to
		// reliably download the device owner APK from an HTTPS connection.
		properties.put(
				DevicePolicyManager.EXTRA_PROVISIONING_LOCAL_TIME,
				String.valueOf(System.currentTimeMillis())
		);
		// To calculate checksum execute command (taken from http://stackoverflow.com/questions/26509770/checksum-error-while-provisioning-android-lollipop):
		// cat Something.apk | openssl dgst -binary -sha1 | openssl base64 | tr '+/' '-_' | tr -d '='
		properties.put(
				DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM,
				"[Device owner app checksum]"
		);
		properties.put(
				DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION,
				"[Device owner app URL]"
		);

		try {
			properties.store(outputStream, getString(R.string.nfc_comment));
			final NdefRecord record = NdefRecord.createMime(
					DevicePolicyManager.MIME_TYPE_PROVISIONING_NFC,
					outputStream.toByteArray()
			);

			return new NdefMessage(new NdefRecord[]{record});
		} catch (final IOException e) {
			throw new RuntimeException(e);
		}
	}
}

Обратите внимание на следующие моменты:

  • Не забудьте добавить <uses-permission android:name="android.permission.NFC" /> в манифест;
  • Вам нужно указать package name вашего приложения – device owner-a (в нашем случае это «com.example.deviceowner»)
  • Вам нужно указать правильный URL по которому доступен apk-файл приложения – device owner-a (вместо [Device owner app URL])
  • А также вам нужно указать контрольную сумму (вместо [Device owner app checksum]). Контрольная сумма считается коммандой cat Something.apk | openssl dgst -binary -sha1 | openssl base64 | tr '+/' '-_' | tr -d '=' (команда взята отсюда)

Эксперименты


Оба приложения надо собрать, инсталлятор установить на одно устройство, device owner надо выложить на сервер, целевое устройство wipe-нуть. После этого можно начать эксперименты.

Для демонстрации работы функции скрытия приложения я скрою системные настройки на целевом устройстве:


Здесь мы видим, что настройки есть в списке приложений.


Здесь мы видим, что настройки есть в списке последних использованных приложений.


Выключаем приложение – настройки.


Настройки исчезли из списка последних использованных приложений.


И из списка приложений.


Даже если вы попытаетесь запустить настройки из “шторки” (обведено зеленым прямоугольником) и другими способами у вас ничего не получится.

Ссылки на проекты


Исходные коды проектов приложений:

github.com/raynor73/DeviceOwner
github.com/raynor73/NfcProvisioning
Теги:
Хабы:
Всего голосов 11: ↑11 и ↓0+11
Комментарии5

Публикации

Истории

Работа

Ближайшие события