Android 7.0の適切なインストール、写真、裁断、画像選択

16562 ワード

前言
Android 7.0システムでは、androidフレームワークがStrictMode APIポリシーを強制的に実行し、file://URIをアプリケーションに公開することを禁止しています.ファイルfile://URIタイプのIntentがアプリケーションから離れた場合、アプリケーションに失敗し、システムカメラを呼び出して写真を撮ったり、写真をカットしたりするFileUriExposedException異常が発生します.
1.写真を撮る
7.0より前:
 public static void startTakePhoto(Activity activity) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(
                MediaStore.EXTRA_OUTPUT,
                Uri.fromFile(new File(Utils.IMAGE_FOLDER
                        + Utils.TEMP_IMAGE_NAME)));
        activity.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
    }

この場合、Android 7.0以上のオリジナルシステムを使用して再度実行すると、android.os.FileUriExposedExceptionが投げ出されたアプリケーションが直接停止していることがわかります.
Caused by: android.os.FileUriExposedException: 
    file:///storage/emulated/0/20170601-030254.png 
        exposed beyond app through ClipData.Item.getUri()
    at android.os.StrictMode.onFileUriExposed(StrictMode.java:1932)
    at android.net.Uri.checkFileUriExposed(Uri.java:2348)

理由は公式サイトで説明されています.
Android 7.0向けのアプリケーションでは、Androidフレームワークで実行されるStrictMode APIポリシーは、アプリケーションの外部でfile://URIを公開することを禁止します.ファイルURIを含むintentがアプリケーションから離れた場合、アプリケーションに障害が発生し、FileUriExposedException異常が発生します.
同様に、公式サイトでもソリューションが提供されています.
アプリケーション間でファイルを共有するには、content://URIを送信し、URIに一時的なアクセス権を付与します.この認証を行う最も簡単な方法は、FileProviderクラスを使用することです.アクセス権および共有ファイルの詳細については、「共有ファイル」を参照してください.https://developer.android.com/about/versions/nougat/android-7.0-changes.html#accessibility
2.FileProvider互換撮影
FileProviderは実際にContentProviderのサブクラスであり、その役割も明らかになっています.file:///Uri使わないなら、Uriをcontent://に変えてください.
https://developer.android.com/reference/android/support/v4/content/FileProvider.html
全体的な実装手順:(1)providerを宣言する:

    


注意してください.meta-dataを設定し、xmlファイルを指します.
(2)resource xml fileの作成


    
    
    
    
    
     


pathsノードの内部では、次のサブノードがサポートされています.
デバイスを表すルートディレクトリnew File("/");代表context.getFilesDir()代表context.getCacheDir()代表Environment.getExternalStorageDirectory()代表context.getExternalFilesDirs()代表getExternalCacheDirs()
各ノードは2つのプロパティをサポートしています:name , path仮想パスでファイルパスをマッピングする必要があるため、xmlファイルを作成し、pathおよびxmlノードでアクセス可能なディレクトリを決定し、nameプロパティで実際のファイルパスをマッピングする必要があります.
(3)FileProvider APIの使用
 public static void startTakePhoto(Activity activity) {
      Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
      Uri fileUri = FileProvider.getUriForFile(this, "com.lqwawa.internationalstudy.fileprovider", file);
     intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
     activity.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
    }

次に生成されたuriを見てみましょう
content://com.lqwawa.internationalstudy.fileprovider/external/20170601-041411.png
フォーマットは次のとおりです.content://authorities/定義されたnameプロパティ/ファイルの相対パス、すなわちnameが格納可能なフォルダパスを非表示にします.今は7.0のオリジナル携帯電話で正常に動作しています
しかし、4.4のシミュレータを開き、上記のコードを実行すると、またCrashが出てきて、Permission Denial~
Caused by: java.lang.SecurityException: Permission Denial: opening provider android.support.v4.content.FileProvider from ProcessRecord{52b029b8 1670:com.android.camera/u0a36} (pid=1670, uid=10036) that is not exported from uid 10052
at android.os.Parcel.readException(Parcel.java:1465)
at android.os.Parcel.readException(Parcel.java:1419)
at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:2848)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:4399)

低バージョンのシステムは、これを普通のProviderとして使用しているだけなので、私たちは許可していません.contentproviderのexport設定もfalseです.Permission Denialを引き起こす.
では、exportをtrueに設定できますか?
残念ながらできません.
FileProviderの内部:
@Override
public void attachInfo(Context context, ProviderInfo info) {
    super.attachInfo(context, info);

    // Sanity check our security
    if (info.exported) {
        throw new SecurityException("Provider must not be exported");
    }
    if (!info.grantUriPermissions) {
        throw new SecurityException("Provider must grant uri permissions");
    }

    mStrategy = getPathStrategy(context, info.authority);
}

exportedはfalse、grantUriPermissionsはtrueでなければならないことが判明しました~~
だから唯一の方法は授権です~
contextは2つの方法を提供します.
  • grantUriPermission(String toPackage, Uri uri, int modeFlags)
  • revokeUriPermission(Uri uri, int modeFlags);

  • grantUriPermissionは、どのアプリケーションに権限を与えるかというパッケージ名を渡す必要がありますが、共有など、エンドユーザーがどのappを選択するか分からない場合が多いので、このようにすることができます.
    public void takePhotoNoCompress(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
    
            String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                    .format(new Date()) + ".png";
            File file = new File(Environment.getExternalStorageDirectory(), filename);
            mCurrentPhotoPath = file.getAbsolutePath();
    
            Uri fileUri = FileProvider.getUriForFile(this, "com.galaxyschool.app.wawaschool.fileprovider", file);
    
            List resInfoList = getPackageManager()
                    .queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
            for (ResolveInfo resolveInfo : resInfoList) {
                String packageName = resolveInfo.activityInfo.packageName;
                grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            }
    
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
        }
    }
    

    これで終わりますが、面倒です.古いシステムと互換性を持っているだけなら、バージョンチェックをすることをお勧めします.つまり、権限を気にしないで、uriを直接取得することをお勧めします.
    Uri fileUri = null;
    if (Build.VERSION.SDK_INT >= 24) {
        fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
    } else {
        fileUri = Uri.fromFile(file);
    }
    

    3.裁断
    FileProviderHelpは互換性処理を行った.
     /**
         * @param activity      activity
         * @param orgUri           Uri /////7.0   content://
         * @param desUri             Uri /////   file:///
         * @param aspectX     X     
         * @param aspectY     Y     
         * @param width              
         * @param height            
         * @param requestCode         
         */
        public static void startZoomPhoto(Activity activity, File orgUri, File desUri, int aspectX, int aspectY, int width, int height, int requestCode) {
            doCrop(activity, FileProviderHelp.getUriForFile(activity,orgUri),
                    Uri.fromFile(desUri), aspectX, aspectY, width, height, requestCode);
        }
    
        private static void doCrop(Activity activity, Uri orgUri, Uri desUri, int aspectX, int aspectY, int width, int height, int requestCode) {
            Intent intent = new Intent("com.android.camera.action.CROP");
            FileProviderHelp.setIntentDataAndType(activity,
                    intent, "image/*", orgUri, true); /////7.0   content://
            intent.putExtra("crop", "true");
            intent.putExtra("aspectX", aspectX);
            intent.putExtra("aspectY", aspectY);
            intent.putExtra("outputX", width);
            intent.putExtra("outputY", height);
            intent.putExtra("scale", true);
            //           Uri 
            intent.putExtra(MediaStore.EXTRA_OUTPUT, desUri);   /////   file:///
            intent.putExtra("return-data", false);
            intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
            intent.putExtra("noFaceDetection", true);
    
            activity.startActivityForResult(intent, requestCode);
        }
    
    

    4.アルバムの選択
     /**
         * @param activity      activity
         * @param requestCode         
         */
        public static void startFetchPhoto(Activity activity, int requestCode) {
            Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
            photoPickerIntent.setDataAndType(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    "image/*");
            activity.startActivityForResult(photoPickerIntent, requestCode);
        }
    

    4.4以降に選択した画像は実際のUriではなくカプセル化されたUriであるため,4.4以上でこのUriを解析する.
     case ActivityUtils.REQUEST_CODE_FETCH_PHOTO:
                    if (data != null) {
                        if (BasicUserInfoActivity.this == null) {
                            return;
                        }
                        String photo_path = null;
    
                        photo_path = PhotoUtils.getImageAbsolutePath(BasicUserInfoActivity.this, data
                                .getData());
    
                        if (TextUtils.isEmpty(photo_path)) {
                            return;
                        }
                        PhotoUtils.startZoomPhoto(BasicUserInfoActivity.this,new File(photo_path));
                    }
                    break;
    

    5.FileProvider互換インストールapkを使用する
    通常、インストールapkを作成するときは、次のようにします.
    public void installApk(View view) {
        File file = new File(Environment.getExternalStorageDirectory(), "testandroid7-debug.apk");
    
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(Uri.fromFile(file),
                "application/vnd.android.package-archive");
        startActivity(intent);
    }
    

    7.0のオリジナル携帯を持って走って、android.os.FileUriExposedExceptionがまた来ました~~
    uriの取得方法を簡単に変更します.
    if (Build.VERSION.SDK_INT >= 24) {
        fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
    } else {
        fileUri = Uri.fromFile(file);
    }
    

    もう一度走ってみたら、意外にも異常を投げ出した(警告、Crashなし):
    java.lang.SecurityException: Permission Denial: 
    opening provider android.support.v4.content.FileProvider 
            from ProcessRecord{18570a 27107:com.google.android.packageinstaller/u0a26} (pid=27107, uid=10026) that is not exported from UID 10004
    

    権限の問題であることがわかりますが、権限についてgrantUriPermissionという方法を話したばかりですが、この方法はもちろん問題ありません.
    プラスして運転すればいいです.
    権限については、次のような方法も提供されています.
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    

    パッケージをインストールする前に上記のコードを付けて、再び正常に動作することができます~
    2つの疑問があります
    質問1:なぜさっき写真を撮った時、Android 7の設備はPermission Denialの問題に遭遇しなかったのですか?
    権限が不要なのは、主にIntentのactionがACTION_IMAGE_CAPTUREは、startActivityの後、InstrumentationのexecStartActivityメソッドを転々と呼び出し、このメソッドの内部でintent.migrateExtraStreamToClipData()を呼び出します.方法.このメソッドには、次のものが含まれます.
    if (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
            || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action)
            || MediaStore.ACTION_VIDEO_CAPTURE.equals(action)) {
        final Uri output;
        try {
            output = getParcelableExtra(MediaStore.EXTRA_OUTPUT);
        } catch (ClassCastException e) {
            return false;
        }
        if (output != null) {
            setClipData(ClipData.newRawUri("", output));
            addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION);
            return true;
        }
    }
    

    私たちのEXTRAを見ることができますOUTPUTは、setClipDataに移行し、WRITEとREAD権限を直接追加してくれました.この部分の論理は21以降に追加されるべきである.
    質問2:なぜさっき写真を撮ったケースでAndroid 4.4デバイスが権限の問題に遭遇し、addFlagsのような方法で解決しなかったのですか?
    addFlagsは主にsetDataに用いられるため、setDataAndTypeおよびsetClipData(注:4.4の場合、ACTION_IMAGE_CAPTUREをsetClipDataに変換して実装していない)という方式である.
    だからaddFlags方式はACTION_IMAGE_CAPTUREは5.0以下では無効なのでgrantUriPermissionを使用する必要があります.setDataで共有されているuriであればaddFlagsを使用するのは問題ありません(簡単な例を書いてテストし、2つのappがインタラクティブでcontent://).
    まとめクイックフィット7.0
    (1)新しいmodule(プロジェクト中のelearning-library)
    AndroidManifest.xmlでFileProviderの登録を完了し、コードは次のように記述されています.
    
        
            
        
    
    

    注意してください.android:authoritiesは、libraryが最終的に複数のプロジェクトを参照させる可能性がありますが、android:authoritiesは繰り返してはいけません.2つのappで同じことが定義されている場合、後者は携帯電話にインストールできません.
    同じ作成file_paths~
    
    
        
        
    
        
    
        
    
        
        
    
    
    
    

    最後に、次のような補助クラスを作成します.
    public class FileProviderHelp {
    
        public static Uri getUriForFile(Context context, File file) {
            Uri fileUri = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                fileUri = getUriForFile24(context, file);
            } else {
                fileUri = Uri.fromFile(file);
            }
            return fileUri;
        }
    
        public static Uri getUriForFile24(Context context, File file) {
    
            Uri fileUri = android.support.v4.content.FileProvider.getUriForFile(context,
                    context.getPackageName() + ".fileprovider",
                    file);
            return fileUri;
        }
    
    
        public static void setIntentDataAndType(Context context,
                                                Intent intent,
                                                String type,
                                                File file,
                                                boolean writeAble) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intent.setDataAndType(getUriForFile(context, file), type);
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                if (writeAble) {
                    intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                }
            } else {
                intent.setDataAndType(Uri.fromFile(file), type);
            }
        }
    

    (2)使用
    どのアイテムが7.0に適合する必要があるかは、このライブラリをこのように参照し、コードを1行変更するだけで適合を完了します.たとえば、次のようにします.
      /**
         * @param activity      activity
         * @param imageUri             
         * @param requestCode          
         */
        public static void startTakePhoto(Activity activity, File imageUri, int requestCode) {
            //      
            Intent intentCamera = new Intent();
            intentCamera.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//  Action   
            intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, FileProviderHelp.getUriForFile(activity,imageUri));//           URI
            activity.startActivityForResult(intentCamera, requestCode);
        }
    

    変えるだけ
     intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, FileProviderHelp.getUriForFile(activity,imageUri));//           URI
    

    インストールapk同様の変更setDataAndTypeは次のとおりです.
    public static void installApp(Context context, String filePath) {
            Intent intent = new Intent(Intent.ACTION_VIEW);
    // setFlags()     addFlags()  ,      
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            FileProviderHelp.setIntentDataAndType(context,
                    intent, "application/vnd.android.package-archive", new File(filePath), true);
            try {
                context.startActivity(intent);
            } catch (ActivityNotFoundException e) {
                e.printStackTrace();
            }
        }
    

    本文は張鴻洋のブログAndroid 7.0行動変更からFileProviderを通じてアプリケーション間でファイルを共有しよう