【Android P】Intent.ACTION_UNINSTALL_PACKAGEを飛ばしてアプリをアンインストールする際の注意点

#概要
アプリ内からボタンイベント等で他のアプリのアンインストールを実行する際に`Intent.ACTION_UNINSTALL_PACKAGE`を飛ばしますが、この処理を実装しているアプリをAndroid Pで実行した場合、そのままではアンインストール処理が走らないケースがあります。メモ書きとしてここに記します。

#権限の追加
`Intent.ACTION_UNINSTALL_PACKAGE`を使う処理をアプリ内に実装する場合、`android.permission.REQUEST_DELETE_PACKAGES`をmanifest内で宣言する必要があります。

```xml:AndroidManifest.xml
    <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
```

`android.permission.REQUEST_DELETE_PACKAGES`自体はAPI level 26(Oreo)で実装されたものですが、API level 28以降をtargetSdkVersionとするアプリにおいて、`Intent.ACTION_UNINSTALL_PACKAGE`の使用に際して必須となりました。

- [REQUEST_DELETE_PACKAGES](https://developer.android.com/reference/android/Manifest.permission.html#REQUEST_DELETE_PACKAGES)

> Allows an application to request deleting packages. Apps targeting APIs Build.VERSION_CODES.P or greater must hold this permission in order to use Intent.ACTION_UNINSTALL_PACKAGE or PackageInstaller.uninstall(VersionedPackage, IntentSender).

ここで一点注意しなければいけないのが、"must hold this permission in order to use Intent.ACTION_UNINSTALL_PACKAGE"という部分です。実際に権限を要求するのは内部で使われる`UninstallerActivity.class`であり、同クラスを使用する`Intent.ACTION_DELETE`においても同様のパーミッション宣言が必要です。

今回の権限追加によるAndroid P対応について、以下で検証してみます。

#検証

実行環境はAndroidStudio3.1.3、エミュレータはAPI 27とAPI 28の2つを使用します。

アンインストールの対象となるSampleAppと、Intentを投げてアンインストール処理を呼び出すRemoveAppを作成します。
SampleAppはAndroidStudioが生成した状態のまま使用、RemoveAppには`activity.main`にRemoveButtonを配置し、`MainActivity.java`にてonClickメソッドを実装します。
targetSdkVersionを28に設定します。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/remove_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClickRemoveButton"
        android:text="REMOVE"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
```

```java:MainActivity.java
package jp.co.example.www.removeapp;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

public class MainActivity extends AppCompatActivity {

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

    public void onClickRemoveButton(View v) {
        Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, Uri.fromParts("package", "jp.co.example.www.sampleapp", null));
        startActivity(intent);
    }
}
```
まず、manifestにpermissionを宣言しない状態で検証してみます。

##API 27における動作
作成した2つのアプリがインストールされた状態で、RemoveApp上でボタンをタップします。
![Slide1.jpg](https://qiita-image-store.s3.amazonaws.com/0/230368/a8ed05db-d28e-f0d5-efbd-9ec0262bb613.jpeg)
当然ダイアログが表示されます。

##API 28における動作
上記と同様、作成した2つのアプリをインストールしRemoveAppをPで実行してみます。
RemoveAppのRemoveボタンをタップします。
![Slide1.jpg](https://qiita-image-store.s3.amazonaws.com/0/230368/64068ab0-fee0-15c3-e33d-da1477dc6369.jpeg)
何も起きません。ログを見てみましょう。

```
07-31 12:45:44.775 4372-4372/? E/UninstallerActivity: Uid 10082 does not have android.permission.REQUEST_DELETE_PACKAGES or android.permission.DELETE_PACKAGES
```

UninstallerActivityがpermissionを要求しています。manifestに書き加えましょう。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.co.example.www.removeapp">

    <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />

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

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```

再び実行してみます。
![Slide1.jpg](https://qiita-image-store.s3.amazonaws.com/0/230368/f8d7d553-9e74-e248-aba5-76839ada989c.jpeg)
ばっちり！

#ちなみに
上記のエラーログはログキャットのフィルターを「選択したアプリケーションのみ」にしていると出てきません。気付きにくいですね。