AndroidアプリをついにAPIレベル26に対応せざるを得ない状況になり対応しましたので、メモを残しておきたいと思います。

機能もそれほど多くなく地味なアプリなので、必要最低限のAPIレベルに対応していればよかったので、これまでは targetSdkVersion=7 という状態でしたが、どうにも修正しなければならないバグが発覚したので、GooglePlayストアにアップロードするために targetSdkVersion=26 にしなければならなくなりました。(これまで target=7 でもアップロードできていた事自体が驚きかもしれませんが)

まずは、とりあえずバグだけ修正したものを、しらばっくれてストアにアップロードしてみましたが、やはりダメでした。

アップロードできませんでした
現在、お客様のアプリはAPIレベル7を対象にしています。セキュリティとパフォーマンスが最適化された最新のAPIを利用するには、APIレベル26以上を対象にする必要があります。アプリの対象APIレベルを26以上に変更してください。

予想通りの結果ではあります。とりあえず targetSdkVersion=26 にしてコンパイルし直します。

ネットワークアクセスの修正

コンパイルし直してからアプリを起動してみると、とりあえず起動するもののネットワークアクセスを行う箇所で落ちているようです。

java.net.SocketException: android.os.NetworkOnMainThreadException

ググってみたところ、ネットワーク処理をメインスレッドで行うと例外が発生するということで、下記のコードを追加するとエラーにならないということなので追加しました。

StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);

ネットワーク処理をメインスレッドで行えなくなったのが Android3.0 からだそうで、ずいぶん昔のままのソースコードを使っていたことになります。

ファイル読み書きの権限の修正

APKファイルをPlayストアにアップロードしておいたところ自動テストが行われたということで、いくつかエラーが報告されていました。

java.lang.SecurityException: MODE_WORLD_READABLE no longer supported

このアプリでは、プリファレンスを作成するときは MODE_PRIVATE で作成するようになっていたのですが、ファイルを作るときに MODE_WORLD_READABLE が指定されていたために例外が発生していたようです。

MODE_WORLD_READABLE が指定されていた箇所を MODE_PRIVATE に修正しました。

URIの公開を無視

Android API Level 24 より、Intent を使ってファイルの共有が大幅に制限されたことにより、ファイルを読み書きする時点で例外が発生することがあります。

android.os.FileUriExposedException: file:///storage/emulated/0/test.txt exposed beyond app throutgh Intent.getData()

URIの公開を無視する以下のコードを追加しました。

StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());

クラス変数がnullになってしまった

これは API Level を変更したことによるものではないのかもしれませんが、クラス変数が null のまま処理が進んでしまうことがあり、実行中に NullPointerException が発生してしまっていました。

java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference

コード内で該当の変数がnullかどうかチェックして処理を振り分けるようにしました。

loadUrl() をメインスレッドで呼び出すことができなくなった

mWebView.loadUrl(urlstr); をメインスレッドで呼び出すことができなくなりました。

A WebView method was called on thread 'xxx'. All WebView methods be called on the same thread. (Expected Looper Looper {xxx} called on Looper {xxx}, FYI main Looper is Looper {xxx}

post()を介して実行するように書き換えます。

mWebView.post(new Runnable()){
@Override
public void run() {
mWebView.loadUrl(urlstr);
}
});

レイアウトXMLのID指定の厳密化

レイアウトXMLでウィジェットに対して指定しているIDが、ファイル内にユニークでないとエラーになるようになりました。以前は、入れ子状態などで親が異なっていれば同じIDであっても、階層を指定すれば findViewById などで取得することが出来たのですが、同じIDは階層に関係なくファイル内に1個しか存在できなくなったようです。

findViewById(R.id.area1).findViewById(R.id.icon).setVisibility(View.VISIBLE);
findViewById(R.id.area2).findViewById(R.id.icon).setVisibility(View.INVISIBLE);

area1のiconと、area2のiconを別々のものとして扱うことが出来たのですが、iconが重複して指定されているということでエラーになりますので、下記のように修正します。レイアウトXMLの対応する箇所も修正します。

findViewById(R.id.area1).findViewById(R.id.icon1).setVisibility(View.VISIBLE);
findViewById(R.id.area2).findViewById(R.id.icon2).setVisibility(View.INVISIBLE);

初回起動時の権限要求の変更

今回行った API Level 26 対応で最も大きい変更と言えるかもしれません。以前は、AndroidManifest.xml ファイルに必要なパーミッションを書いておけば、アプリ初回起動時にまとめて許可を得ることができました。

Android6.0からは、Normal Permission と Dangerous Permission に分けられ、Normal Permission はアプリを起動するだけで暗黙的に付与されるようになり、Dangerous Permission はアプリがひとつずつ許可を求めるようになりました。

今回のアプリでは、外部ストレージへのアクセスと、電話へのアクセス(デバイスIDを参照しているため)が必要になったため、それぞれの権限について許可を求め、許可されなかった場合はその場でアプリを終了するようなロジックを作成しました。

// パーミッションをリクエストする(Android6.0以降対応)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
  //ストレージが許可されていない
  //ストレージの許可を求めるダイアログを表示します。
  ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
}else{
  //ストレージが許可されている
  if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
  //電話が許可されていない
  //電話の許可を求めるダイアログを表示します。
  ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_PHONE_STATE}, 1);
  }

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults){
  switch(requestCode){
    case 0: { //WRITE_EXTERNAL_STORAGEの許可
      if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        //許可されたら内容をチェック
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
          //ストレージが許可されなかったらアプリ終了
          quitIfNotGranted();
        } else {
          //ストレージが許可された時は、つづけて READ_PHONE_STATUS の許可を得る
          if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            //電話の許可を求めるダイアログを表示します。
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_PHONE_STATE}, 1);
          }
        }
      } else {
        //ストレージが許可されなかったらアプリ終了
        quitIfNotGranted();
      }
      break;
    }
    case 1: { //READ_PHONE_STATEの許可
      if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        //許可されたら内容をチェック
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
          //電話が許可されなかったらアプリ終了
          quitIfNotGranted();
        }else{
          //パーミッションが2つとも許可された場合は何もせず次に進みます(アプリを終了しません)
        }
      }else{
        //電話が許可されなかったらアプリ終了
        quitIfNotGranted();
      }
      break;
    }
  }
}

public void quitIfNotGranted() {
  //ユーザーによってパーミッションが許可されなかった場合はアプリを終了する
  AlertDialog.Builder builder = new AlertDialog.Builder(this);
  builder.setMessage("許可されない場合はアプリを使用できません。").setPositiveButton("終了",new DialogInterface.OnClickListener() {
      public void onClick(DialogInterface dialog, int id) {
        //アプリ終了
        finish();
      }
    });
  builder.show();
}

ビルド環境の変更

起動時に権限を要求するロジックを作成するためには、下記のパッケージを読み込む必要があるのですが、どうやっても見つからないというエラーになってしまって読み込めないので、Gradleを導入することにしました。

import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;

一番最初の開発環境はEclipseでしたが、Android Studio に移行したときから Gradle への移行がサジェストされていたのですが、ずっと無視し続けていました。

実際にどのようにすればGradleに移行できるのかわからなかったのでいろいろと試行錯誤したのですが、新しいプロジェクトを作って古いプロジェクトファイルをインポートしたところ、うまくビルドできるようになりました。

上記のパッケージを読み込むためにapp.gradleに下記の記述を追加します。また、古いアプリで使われているorg.apache.httpパッケージを有効にする行をandroidセクションの中に追加します。

android {
  ...
  useLibrary 'org.apache.http.legacy'
}

dependencies {
  compile 'com.android.support:support-v4:26.0.0-alpha1'
  compile 'com.android.support:appcompat-v7:26.0.0-alpha1'
}

サポート対象外の警告

Playストアにアップロードしたアプリに対して、下記のような警告が表示されているのですが、いまのところ対処方法がよくわからないので、そのままになっています。

8件の警告があります
以下のAPIはグレイリストに登録されているため、Googleでは、既存バージョンのAndroidでの動作を保証することができません。一部のAPIは対象のSDKですでに使用が制限されている場合があります。
API Landroid/view/textclassifier/logging/SmartSelectionEventTracker$SelectionEvent;>selectionAction(III)Landroid/view/textclassifier/logging/SmartSelectionEventTracker$SelectionEvent;

とりあえず、この渓谷を無視した状態でPlayストアに内部テスト版としてアップロードしテストができるようになりました。

カテゴリー: 開発関係

0件のコメント

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください