DeviceAdmin, ProfileOwner, DeviceOwnerについての初めの一歩

CookpadやSlack, メルカリなどのtoC向けのアプリでなく、業務に直接使う業務用アプリを構築する時にはtoC向けのアプリで想定されるものとは別な問題が発生する。

- 端末を紛失した時に第３者に勝手に操作されないか？データを盗み見たりされないか。
- 端末使用者が勝手にGooglePlayなどからゲームをDLして遊んだりするのを伏せぎたい。
- 展示用の端末について、勝手に設定変えられたくない。
- システムの更新のためにわざわざ技術者を現地に派遣なければならない。

普通のAndroidのフレームワークでやろうとするとかなりトリッキーなことをしたりしないといけない。今回はこれらの問題を楽に解決するための端末管理(Device Policy)の権限、Device Admin, Profile Owner, Device Ownerについての紹介、実際どうやって使うかを提示していく。

# Device Admin, Profile Owner, Device Ownerとは?

## 概要
- Device Admin
ちょっとしたMDM機能を持ったアプリを作る時に使うための権限。画面ロックとかカメラの使用させないなどのちょっとした管理者機能が使える。

- Profile Owner 
個人の端末を業務用として使う時に用いる権限。Device Adminよりも強い権限。業務用アカウントを作ってそこだけ業務に必要な制約を加える。(BYOD支援)

- Device Owner
企業管理の端末に使用する権限。Profile Ownerよりも強い権限。業務専用タブレットとか図書館などの受付タッチパネルなどに応用される。

## 各権限の特徴

|権限名|特徴|
|:-----------------|:------------------|
|Device Admin| ・Android4.4以前でも使える<BR>・複数アプリに付与できる。<BR>・UI, API, adbを使って設定・解除ができる。|
|Profile Owner| ・Android5.0から使える<BR>・1ユーザにつき1アプリしか付与できない。<BR>・API, adbを使わないと付与できない。<BR>・(Android7.0から)APIを使って解除可能|
|Device Owner |・Android5.0から使える<BR>・1端末につき1アプリしか付与できない。<BR>・adb及び端末初期化時の特殊な方法でしか付与できない。<BR>・APIを使って解除可能|

## 各権限でできること

ここでは一部だけ抜粋します。もっと知りたい場合は
https://developer.android.com/reference/android/app/admin/DevicePolicyManager.html
を参考にしてみてください。

### Device Admin
- APIからの画面ロック(DevicePolicyManager#lockNow)
- カメラの無効化(DevicePolicyManager#setCameraDisabled)
- ロック解除パスワードの品質(パスワードの長さなど)設定(DevicePolicyManager#setPassword* というメソッド2つ)

### Profile Owner
Device Adminでできることに加えて・・・

- 指定したアプリを利用できないようにする(DevicePolicyManager#setApplicationHidden)
- スクリーンキャプチャの禁止

### Device Owner
Device Admin, Profile Ownerでできることに加えて・・・

- (Android6.0から)アプリのサイレントインストール
- (Android6.0から)RuntimePermissionの許可設定ポリシーの変更
- GPSの有効化

※ Device Ownerでできることなどは
http://qiita.com/bina1204/items/eb6cb133bc833dc886f7
にも書いてあります。

# 具体的な手順と実装サンプル

以下に具体的な設定手順、および機能を実現するためのサンプルコードを記載していく。なお、コードについては
https://github.com/LyricalMaestro/DeviceOwnerSample
にあげています。

## 設定・解除手順

### アプリにDeviceAdmin権限をつける手順
(1) 該当アプリにDeviceAdminReceiverを継承したクラスを作成します。具体的には以下のようなコードを作成・追記します。

```java:MyDeviceAdminReceiver.java
public class MyDeviceAdminReceiver extends DeviceAdminReceiver {

    @Override
    public void onEnabled(Context context, Intent intent) {
        super.onEnabled(context, intent);
    }
}
```

```AndroidManifest.xml
・・・略
 <receiver 
  android:name=".MyDeviceAdminReceiver"
  android:permission="android.permission.BIND_DEVICE_ADMIN">
     <meta-data
          android:name="android.app.device_admin"
          android:resource="@xml/admin" />
     <intent-filter>
         <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
     </intent-filter>
 </receiver>

・・・略
```

```res/xml/admin.xml
<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-policies>
        <force-lock />
    </uses-policies>
</device-admin>
```

(2) 「設定」アプリ-「セキュリティ」-「端末管理アプリ」に飛んでチェックを入れる。
  
※ `adb shell dpm set-device-admin [パッケージ名]/[DeviceAdminReceiver.javaのパス]`を実行してもOK.

### アプリにProfileOwner権限をつける手順

こちらを参考にしてください。。(すいません、サンプル作る暇なかったのと、ユーザ削除以外解除方法思いつかなかったので・・・後日詳しい方法を書いて提示します。)
http://gsbina.com/?p=1672

※ `adb shell dpm set-profile-owner [パッケージ名]/[DeviceAdminReceiver.javaのパス]`を実行してもOK.ただし、GooglePlayアカウントなど登録されていたらうまくいかないです。

### アプリにDeviceOwner権限をつける手順
まず、「DeviceAdmin」設定の(1)と同じことをする。そのあとは、端末によって変わる。

- SIMフリーの場合
`adb shell dpm set-device-owner [パッケージ名]/[DeviceAdminReceiver.javaのパス]`を実行すればOK.ただし、GooglePlayアカウントなど登録されていたらうまくいかない。

- キャリア製の端末の場合(docomoアカウントなど絶対に消せないアカウントがある場合)
正直かなり面倒くさいです。もう1台端末、プロビジョニング用のアプリを用意し、かつ端末のデータ初期化・・・とやらないといけないです。ここに詳細な手順書くとページをおもいっきり使ってしまうのでここでは割愛。
http://gsbina.com/?p=1678
などを参考にするといいと思います。

### アプリからDeviceOwner権限をなくす手順
DeviceOwner設定されたアプリから`DevicePolicyManager#clearDeviceOwner`を実行すればOKです。

```java:MainActivity.java
mClearDeviceOwnerBtn = (Button) findViewById(R.id.clear_device_owner_btn);
mClearDeviceOwnerBtn.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
      /**
       * DeviceOwnerの解除
       */
       mDpm.clearDeviceOwnerApp(getPackageName());
       setButtonEnable(false);
     }
  });
mDpm = (DevicePolicyManager) getSystemService(DEVICE_POLICY_SERVICE);
```

## 具体的なオペレーションを実現するための実装サンプル

### サイレントインストール
実装的にはPackageInstallerを用います。
サンプルコードは、
https://github.com/LyricalMaestro/DeviceOwnerSample/blob/master/app/src/main/java/com/lyricaloriginal/deviceownersample/ApkInstaller.java 
にあります。

### RuntimePermissionの許可設定ポリシーの変更
`DevicePolicyManager#setPermissionPolicy`を使えばOKです。
ただし、Permissionの許可を求めるダイアログが表示されずに許可されるが、M Permisson対応のコードを書かないでそのまま動くということではないです!Runtime Permission対応のコードはきちんと書くようにしてください。(`ActivityCompat.requestPermissions`を実行しないとどのみち許可が下りない)


```java:MainActivity.java
        mPolicyAutoGrantedBtn = (Button) findViewById(R.id.policy_auto_granted_btn);
        mPolicyAutoGrantedBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    ComponentName cn = new ComponentName(MainActivity.this, MyDeviceAdminReceiver.class);
                    mDpm.setPermissionPolicy(cn, DevicePolicyManager.PERMISSION_POLICY_AUTO_GRANT);
                    Toast.makeText(MainActivity.this, "Policy Auto Granted", Toast.LENGTH_SHORT).show();
                }
            }
        });
```

端末初期化したときの設定に戻すときは以下のように実装します。

```java:MainActivity.java(元の許可ポリシーに戻すとき)
        mPolicyPromptBtn = (Button) findViewById(R.id.policy_prompt_btn);
        mPolicyPromptBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    /**
                     * 元のM Permissionのポリシーに戻すときに使う
                     */
                    ComponentName cn = new ComponentName(MainActivity.this, MyDeviceAdminReceiver.class);
                    mDpm.setPermissionPolicy(cn, DevicePolicyManager.PERMISSION_POLICY_PROMPT);
                    Toast.makeText(MainActivity.this, "Policy Prompt", Toast.LENGTH_SHORT).show();
                }
            }
        });
```

### アンインストールアプリ禁止の設定
Profile OwnerでもDevice Ownerでも指定できます。
`DevicePolicyManager#setUnistallBlocked`を使えばOKです。

```java:MainActivity.java
        mSwitchUninstallBlockBtn = (Button) findViewById(R.id.switch_uninstall_block_btn);
        mSwitchUninstallBlockBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ComponentName cn = new ComponentName(MainActivity.this, MyDeviceAdminReceiver.class);
                if (mDpm.isUninstallBlocked(cn, TARGET_UNINSTALL_PACKAGE)) {
                    mDpm.setUninstallBlocked(cn, TARGET_UNINSTALL_PACKAGE, false);
                } else {
                    mDpm.setUninstallBlocked(cn, TARGET_UNINSTALL_PACKAGE, true);
                }
            }
        });
```

# まとめ
業務用アプリ(および端末)を開発する時にこのあたりの機能を使って運用上の問題を回避する方法を実現してみては?

# 参考文献
- DeviceOwnerのお話 (私がpotatotips#32で話した内容)
<BR>http://www.slideshare.net/LyricalMaestro/deviceowner
- Device Owner について<BR>http://qiita.com/bina1204/items/eb6cb133bc833dc886f7
- DeviceOwnerによるアプリのサイレントインストール・アンインストール<BR>http://qiita.com/machortz/items/91a2ffa930cfae02bda0
- Android 5.0 の Profile Owner を有効にする方法<BR>http://gsbina.com/?p=1672