ios WKWebViewでWebViewアプリの作り方まとめとく

iOSで簡単なWebViewアプリの作るときに必要そうなことまとめておく。以下のiOS
android WebViewアプリの作り方まとめとく - とりあえずphpとか

WKWebViewを作ってサイトを表示する

ViewController

import UIKit
import WebKit

class ViewController: UIViewController {

    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        webView = WKWebView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
        view.addSubview(webView)

        let request = URLRequest(url: URL(string: "https://www.google.com")!)
        webView.load(request)
    }
}

クッキーの値を取得する

takuyaokamoto.hateblo.jp

スワイプでブラウザバック

webView.allowsBackForwardNavigationGestures = true

メールや電話の起動リンクに対応

class ViewController: UIViewController, WKNavigationDelegate {
    ・・・
    override func viewDidLoad() {
       ・・・
        webView.navigationDelegate = self
       ・・・
    }

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

        guard let url = navigationAction.request.url else {
            return
        }

        if navigationAction.navigationType == .linkActivated {
            if url.scheme == "mailto" || url.scheme == "tel" {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
                decisionHandler(.cancel)
                return
            }
        }

        decisionHandler(.allow)
    }
}

SSLエラーやhttpとhttpsの混在ページを読み込む

info.plist

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

ViewController

class ViewController: UIViewController, WKNavigationDelegate {
    ・・・
    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
        if challenge.protectionSpace.host == "example.com" && challenge.protectionSpace.serverTrust != nil {
            let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

Pull To Refreshを実装する

class ViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!
    var refreshControll: UIRefreshControl!

    override func viewDidLoad() {
        ・・・
        refreshControll = UIRefreshControl()
        self.webView.scrollView.refreshControl = refreshControll
        refreshControll.addTarget(self, action: #selector(ViewController.refresh(sender:)), for: .valueChanged)
        ・・・
    }


    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.refreshControll.endRefreshing()
    }

    @objc func refresh(sender: UIRefreshControl) {
        guard let url = webView.url else {
            return
        }
        webView.load(URLRequest(url: url))
    }
}

他のサイトは表示したくない

class ViewController: UIViewController, WKNavigationDelegate {
    ・・・
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        ・・・
        if url.host != "example.com" {
            decisionHandler(.cancel)
            return
        }

        decisionHandler(.allow)
    }
}

全ソース

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!
    var refreshControll: UIRefreshControl!

    override func viewDidLoad() {
        super.viewDidLoad()

        webView = WKWebView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
        webView.navigationDelegate = self
        webView.allowsBackForwardNavigationGestures = true
        refreshControll = UIRefreshControl()
        self.webView.scrollView.refreshControl = refreshControll
        refreshControll.addTarget(self, action: #selector(ViewController.refresh(sender:)), for: .valueChanged)
        view.addSubview(webView)

        let url = "https://example.com"
        let request = URLRequest(url: URL(string: url)!)
        webView.load(request)
    }

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

        guard let url = navigationAction.request.url else {
            return
        }

        if navigationAction.navigationType == .linkActivated {
            if url.scheme == "mailto" || url.scheme == "tel" {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
                decisionHandler(.cancel)
                return
            }
        }

        if url.host != "example.com" {
            decisionHandler(.cancel)
            return
        }

        decisionHandler(.allow)
    }

    func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
        if challenge.protectionSpace.host == "example.com" && challenge.protectionSpace.serverTrust != nil {
            let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.refreshControll.endRefreshing()
    }

    @objc func refresh(sender: UIRefreshControl) {
        guard let url = webView.url else {
            return
        }
        webView.load(URLRequest(url: url))
    }
}

androidiosもそこそこ手間でした、、以上です

android WebViewアプリの作り方まとめとく

ただWebサイトを表示するだけのアプリを作ろうとするだけでも意外と実装しないとならないことが多いのがアプリの世界?次回のために作り方をまとめておく

WebViewを作ってサイトを表示する

指定URLを表示するだけのアプリ。JavaScriptは有効にしておく

AndroidManifest.xml

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

MainActivity.java

public class MainActivity extends AppCompatActivity {

    WebView mWebView;

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

        mWebView = (WebView)findViewById(R.id.webView);
	mWebView.loadUrl("https://www.google.com");
	mWebView.getSettings().setJavaScriptEnabled(true);
    }
}

activity_main.xml

<WebView
    android:id="@+id/webView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

リンクに対応する

MainActivity.java

protected void onCreate(Bundle savedInstanceState) {
    ・・・
    mWebView.setWebViewClient(new CustomWebViewClient());
}

class CustomWebViewClient extends WebViewClient {
}

クッキーの有効化

MainActivity.java

protected void onCreate(Bundle savedInstanceState) {
    ・・・
    mWebView.setDomStorageEnabled(new CustomWebViewClient());
}

クッキーの値を取得する

class CustomWebViewClient extends WebViewClient {
    ・・・
    @Override
    public void onPageCommitVisible(WebView view, String url) {
        String cookie = CookieManager.getInstance().getCookie(url);
        Log.d("",cookie);
    }
}

戻るボタンでブラウザバック

MainActivity.java

public class MainActivity extends AppCompatActivity {
    ・・・
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) {
            mWebView.goBack();
        }
        return true;
    }
}

メールや電話の起動リンクに対応

class CustomWebViewClient extends WebViewClient {
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (url.startsWith("mailto:") || url.startsWith("tel:")) {
            Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(url));
            startActivity(intent);
            return true;
	} else {
            return false;
        }
    }
}

SSLエラーやhttpとhttpsの混在ページを読み込む

もちろん不要なら対応しない方がいいけど

・・・
mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
・・・
class CustomWebViewClient extends WebViewClient {
    ・・・
    @Override
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
        handler.proceed();
    }
}

Pull To Refreshを実装する

activity_main.xml

<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</android.support.v4.widget.SwipeRefreshLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {
    ・・・
    SwipeRefreshLayout mRefresh;

    protected void onCreate(Bundle savedInstanceState) {
        ・・・
        mRefresh = (SwipeRefreshLayout) findViewById(R.id.refresh);
        mRefresh.setOnRefreshListener(refreshListener);
    }

    class CustomWebViewClient extends WebViewClient {
        ・・・
        @Override
	public void onPageCommitVisible(WebView view, String url) {
            MainActivity.this.mRefresh.setRefreshing(false);
	}
    }
}

他のサイトは表示したくない

class CustomWebViewClient extends WebViewClient {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (Uri.parse(url).getHost().equals("example.com")) {
            return false;
        } else {
            return true;
        }
    }
}

input type fileに対応する

qiita.com

videoタグのフルスクリーンに対応する

www.monstertechnocodes.com

全ソース

MainActivity.java

package xxx.yyy.zzz;

import android.annotation.SuppressLint;
import android.content.Intent;
import android.net.Uri;
import android.net.http.SslError;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.webkit.CookieManager;
import android.webkit.SslErrorHandler;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class MainActivity extends AppCompatActivity {

    WebView mWebView;
    SwipeRefreshLayout mRefresh;

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

        mRefresh = (SwipeRefreshLayout) findViewById(R.id.refresh);
        mRefresh.setOnRefreshListener(refreshListener);

        mWebView = (WebView)findViewById(R.id.webView);
        mWebView.loadUrl("https://www.google.com");
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.getSettings().setDomStorageEnabled(true);
        mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
        mWebView.setWebViewClient(new CustomWebViewClient());
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) {
            mWebView.goBack();
        }
        return true;
    }

    private SwipeRefreshLayout.OnRefreshListener refreshListener = new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            mWebView.reload();
        }
    };

    class CustomWebViewClient extends WebViewClient {

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if (url.startsWith("mailto:") || url.startsWith("tel:")) {
                Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(url));
                startActivity(intent);
                return true;
            } else if (Uri.parse(url).getHost().equals("example.com")) {
                return false;
            } else {
                return true;
            }
        }

        @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
            handler.proceed();
        }

        @Override
        public void onPageCommitVisible(WebView view, String url) {
            String cookie = CookieManager.getInstance().getCookie(url);
            Log.d("",cookie);

            MainActivity.this.mRefresh.setRefreshing(false);
        }
    }
}

activity_main.xml

<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</android.support.v4.widget.SwipeRefreshLayout>

こんなの実装しないと動かないのかというものがいくつかあるけどそういうものと思うしかない。以上です

旧バージョンのxcodeを使うときに考慮すべきことまとめ

最新のxcodeを使えなくて、実際にアプリ開発で困ったこととその対策をまとめておく

最新のiOSiPhoneXcodeからアプリをインストールできない

iPhoneiOSのバージョンアップを基本的に自動更新にしている人が多いと思うが、xcodeのバージョンは古いままだとxcodeからiPhoneにアプリをインストールできなくなる。その際に必要となるのが以下の記事の作業。Xcodeに含まれていないiOS Devise Supportファイルを手動で入れる作業が必要でその手順が以下に書かれている

kan-kikuchi.hatenablog.com

旧バージョンのSwiftが使用できない

今回実際に困った例ではXcode10.1から10.2へのアップデートでSwiftのバージョンが4.2から5に変わっていたため。プロジェクトでCarthageを使用していてCarthageのコマンドでビルドしたライブラリはSwift5でビルドされる。しかしアプリはSwift4.2で開発したためSwift4.2でビルドしようとするとCarthageでビルドしたものとバージョンが異なるためビルドに失敗する。その際に必要となるのが以下の記事の作業。Xcodeコマンドラインツールのバージョンを切り替える必要があり、その手順が以下に書かれている

sutepulu.com

swiftenvとかのツールもあるのでそれらを使うとそれでも解決するのかもしれないが今回は試していない

AppStoreConnectにアプリをアップロードできない

アップロードする際に以下のようなエラーが発生

ERROR ITMS-90725: "SDK Version Issue. This app was built with the iOS 12.0 SDK. All iOS apps submitted to the App Store must be built with the iOS 11 SDK or later, included in Xcode 9 or later. Further, starting March 2019, all iOS apps submitted to the App Store must be built with the iOS 12.1 SDK or later, included in Xcode 10.1 or later."

iOS12.1 SDK以上を使ってビルドしたものをアップロードしろいう内容。もちろんxcodeのバージョンが新しければ内包されているSDKも新しいのでこのようなエラーは発生しない。対策としては最新のiOSバージョンのSDKをダウンロードして使う。以下の記事に手順が書かれてる

qiita.com

qiita.com

今回はCircleCIでビルドしたアプリをAppStoreConnectへアップロードしたかったため、最新のSDKとversion.plistをgitに登録して、fastlaneのgymでビルドする際にsdkを指定する形で対応した

gym(
  ・・・
  sdk: 'iOS 12.2',
  ・・・
)

【追記】
古いバージョンのXcodeSDKだけ最新にしてもアプリ審査でリジェクトになる場合がある。やはりXcodeのバージョンはどこかのタイミングであげないとダメみたいです。

そんなことより早くxcodeのバージョンあげれるようにしないと...以上です

android 動画のトリミング

今回やりたかったことは指定した時間で動画ファイルを切り出し別の動画ファイルを生成するということ。ググったら、mp4parser使うのがマストなのかと思ってたらAndroid5以上は使わないで実装するみたい。Android5以前の端末で実装したい場合にmp4parserが必要という解釈でいいのかな

https://android.googlesource.com/platform/packages/apps/Gallery2/+/634248d/src/com/android/gallery3d/app/VideoUtils.java
こちらのURLがStackOverflowにリンクされていた

このコードをコピって実際に試したらトリミングできた、以上です

android 動画圧縮ライブラリについて調べたこと

Androidで動画の圧縮をしたくてライブラリを探してみたが思ったより少なかった。iOSだと標準のライブラリであっさりできた記憶があったのに...。試したのはassetsにmp4ファイルを同梱してそれを圧縮してかかった時間と圧縮後のサイズを検証した。どれくらい圧縮するのかの指定がそれぞれあるのかないのかまでまだドキュメントやソースを読み込めておらず、検証方法としてもしかしたら正しくないのかもしれないがとりあえず現時点の結果をメモしておく

検証したライブラリ

android-transcoder(star : 488)
https://github.com/ypresto/android-transcoder

SiliCompressor(star : 471)
https://github.com/Tourenathan-G5organisation/SiliCompressor

VideoCompressor(star : 206)
https://github.com/fishwjy/VideoCompressor

検証端末(シュミレータ含む)と動画ファイル

端末1 : F05-J、CPU 1.2GHz、メモリ 16GB、Android7
端末2 : SO-01G、CPU 2.5GHz、メモリ 32GB、Android6
端末3 : Nexus7、CPU 1.5GHz、メモリ 2GB
端末4 : シュミレータ Android9
端末5 : シュミレータ Android8

動画時間 : 30秒
動画サイズ : 66949244バイト

検証結果

圧縮後のファイルサイズと処理時間は下記のようになった。検証方法として正しいか自信ないけどこの結果だけみると、SiliCompressorがいいのかな。

android-transcoder

圧縮後のサイズ : 約46%
端末1 サイズ : 30962714, 時間 : 33秒
端末2 サイズ : 31064902, 時間 : 18秒
端末3 サイズ : 30941680, 時間 : 63秒
端末4 サイズ : 31392115, 時間 : 28秒
端末5 サイズ : 31371824, 時間 : 32秒

SiliCompressor

圧縮後のサイズ : 約4%
端末1 サイズ : 2316402, 時間 : 32秒
端末2 サイズ : 2315986, 時間 : 26秒
端末3 サイズ : 2661474, 時間 : 23秒
端末4 サイズ : 2651027, 時間 : 26秒
端末5 サイズ : 2673228, 時間 : 25秒

VideoCompressor

https://github.com/fishwjy/VideoCompressor

圧縮後のサイズ : 約10%
端末1 サイズ : 5534250, 時間 : 41秒
端末2 サイズ : 5536037, 時間 : 29秒
端末3 サイズ : 6523136, 時間 : 24秒
端末4 サイズ : 7160129, 時間 : 28秒
端末5 サイズ : 6760777, 時間 : 28秒

ソース

検証に使ったソース。つっこんでいただきたい

android-transcoder
package xxx.yyy.zzz.android_transcoder_demo;

import android.content.res.AssetManager;
import android.os.ParcelFileDescriptor;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.MediaController;
import android.widget.VideoView;

import net.ypresto.androidtranscoder.MediaTranscoder;
import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class MainActivity extends AppCompatActivity {

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

        final long startTime = System.currentTimeMillis();

        final String inputPath = getCacheDir() + "/sample.mp4";
        final String outputPath = getCacheDir() + "/sample_out.mp4";

        File cacheFile = new File(inputPath);

        AssetManager assetManager = getResources().getAssets();
        InputStream inputStream;
        try {
            inputStream = assetManager.open("sample.mp4");
            int size = inputStream.available();
            byte[] buffer = new byte[size];
            inputStream.read(buffer);
            inputStream.close();

            FileOutputStream fileOutputStream = new FileOutputStream(cacheFile);
            fileOutputStream.write(buffer);
            fileOutputStream.close();

            MediaTranscoder.Listener listener = new MediaTranscoder.Listener() {
                @Override
                public void onTranscodeProgress(double progress) {
                }

                @Override
                public void onTranscodeCompleted() {

                    long fileSize = MainActivity.this.getFileSize(inputPath);
                    Log.d("","##### 圧縮前 " + String.valueOf(fileSize));

                    fileSize = MainActivity.this.getFileSize(outputPath);
                    Log.d("","##### 圧縮後  " + String.valueOf(fileSize));


                    long endTime = System.currentTimeMillis();
                    long time = (endTime - startTime) / 1000;
                    Log.d("","##### 処理時間  " + String.valueOf(time));
                }

                @Override
                public void onTranscodeCanceled() {
                }

                @Override
                public void onTranscodeFailed(Exception exception) {
                    exception.printStackTrace();
                }
            };

            MediaTranscoder.getInstance().transcodeVideo(
                    inputPath,
                    outputPath,
                    MediaFormatStrategyPresets.createAndroid720pStrategy(),
                    listener
            );

        } catch (IOException e){
            e.printStackTrace();
        }
    }

    private long getFileSize(String path) {
        File file = new File(path);
        return file.length();
    }
}
SiliCompressor
package xxx.yyy.zzz.silicompressor_demo;

import android.content.Context;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.os.Trace;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.MediaController;
import android.widget.VideoView;

import com.iceteck.silicompressorr.SiliCompressor;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;

public class MainActivity extends AppCompatActivity {

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

        final String inputPath = getCacheDir() + "/sample.mp4";
        final String outputDir = getCacheDir() + "";

        File cacheFile = new File(inputPath);

        AssetManager assetManager = getResources().getAssets();
        InputStream inputStream;

        try {
            inputStream = assetManager.open("sample.mp4");
            int size = inputStream.available();
            byte[] buffer = new byte[size];
            inputStream.read(buffer);
            inputStream.close();

            FileOutputStream fileOutputStream = new FileOutputStream(cacheFile);
            fileOutputStream.write(buffer);
            fileOutputStream.close();

            VideoCompressTask task = new VideoCompressTask(getApplicationContext(), inputPath, outputDir);
            task.execute();

        } catch(IOException e) {
            e.printStackTrace();
        }

    }

    class VideoCompressTask extends AsyncTask<Void, String, String> {

        Context context;
        String inputPath;
        String outputDir;
        final long startTime = System.currentTimeMillis();

        public VideoCompressTask(Context context, String inputPath, String outputDir) {
            this.context = context;
            this.inputPath = inputPath;
            this.outputDir = outputDir;
        }

        @Override
        protected String doInBackground(Void... params) {
            String filePath = null;
            try {
                filePath = SiliCompressor.with(context).compressVideo(this.inputPath, this.outputDir);
            } catch (Exception e) {
                cancel(true);
                e.printStackTrace();
            }
            return filePath;
        }

        @Override
        protected void onPostExecute(String filePath) {
            long fileSize = MainActivity.this.getFileSize(inputPath);
            Log.d("","##### 圧縮前 " + String.valueOf(fileSize));

            fileSize = MainActivity.this.getFileSize(filePath);
            Log.d("","##### 圧縮後  " + String.valueOf(fileSize));

            long endTime = System.currentTimeMillis();
            long time = (endTime - startTime) / 1000;
            Log.d("","##### 処理時間  " + String.valueOf(time));
        }

        @Override
        protected void onCancelled() {
        }
    }

    private long getFileSize(String path) {
        File file = new File(path);
        return file.length();
    }
}

**

package xxx.yyy.zzz.video_compressor_demo;

import android.content.res.AssetManager;
import android.provider.MediaStore;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.MediaController;
import android.widget.VideoView;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import xxx.yyy.zzz.video_compressor_demo.videocompressor.VideoCompress;

public class MainActivity extends AppCompatActivity {

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

        final long startTime = System.currentTimeMillis();

        final String inputPath = getCacheDir() + "/sample.mp4";
        final String outputPath = getCacheDir() + "/sample_out.mp4";

        File cacheFile = new File(inputPath);

        AssetManager assetManager = getResources().getAssets();
        InputStream inputStream;

        try {
            inputStream = assetManager.open("sample.mp4");
            int size = inputStream.available();
            byte[] buffer = new byte[size];
            inputStream.read(buffer);
            inputStream.close();

            FileOutputStream fileOutputStream = new FileOutputStream(cacheFile);
            fileOutputStream.write(buffer);
            fileOutputStream.close();

            VideoCompress.compressVideoLow(inputPath, outputPath, new VideoCompress.CompressListener() {
                @Override
                public void onStart() {
                }
                @Override
                public void onSuccess() {
                    long fileSize = MainActivity.this.getFileSize(inputPath);
                    Log.d("","##### 圧縮前 " + String.valueOf(fileSize));

                    fileSize = MainActivity.this.getFileSize(outputPath);
                    Log.d("","##### 圧縮後  " + String.valueOf(fileSize));


                    long endTime = System.currentTimeMillis();
                    long time = (endTime - startTime) / 1000;
                    Log.d("","##### 処理時間  " + String.valueOf(time));
                }
                @Override
                public void onFail() {
                }
                @Override
                public void onProgress(float percent) {
                }
            });
        } catch (IOException e){
            e.printStackTrace();
        }
    }

    private long getFileSize(String path) {
        File file = new File(path);
        return file.length();
    }
}

以上です

android AsyncTaskの使い方

あまりちゃんと理解してこなかったので自分なりに使い方理解するためのサンプルコードをメモしておく

結果

D/: ##### onPreExecute
D/: ##### doInBackground value1 value2
D/: ##### onProgressUpdate 1秒後に呼ばれる
D/: ##### onProgressUpdate 2秒後に呼ばれる
D/: ##### onProgressUpdate 3秒後に呼ばれる
D/: ##### onPostExecute 結果 value1:value2

ソース

package xxx.yyy.zzz

import android.os.AsyncTask;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

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

        MyTask task = new MyTask();
        task.execute("value1", "value2");
    }

    // パラメータ1 : doInBackgroundに渡す
    // パラメータ2 : onProgressUpdateに渡す
    // パラメータ3 : onPostExecuteに渡す
    class MyTask extends AsyncTask<String, Integer, String> {

        @Override
        // 最初に呼ばれる(任意)
        protected void onPreExecute() {
            Log.d("","##### onPreExecute");
        }

        @Override
        // onPreExecuteの後に呼ばれる(必須)
        protected String doInBackground(String... argv) {
            Log.d("",String.format("##### doInBackground %s %s", argv[0], argv[1]));
            try {
                Thread.sleep(1000);
                publishProgress(1);
                Thread.sleep(1000);
                publishProgress(2);
                Thread.sleep(1000);
                publishProgress(3);
                // cancel(true);
            } catch (InterruptedException e) {
            }
            return argv[0] + ":" + argv[1];
        }

        @Override
        // publishProgressで呼ばれる(任意)
        protected void onProgressUpdate(Integer... progress) {
            Log.d("", String.format("##### onProgressUpdate %d秒後に呼ばれる", progress[0]));
        }

        @Override
        // doInBackgroundが完了したら呼ばれる(任意)
        protected void onPostExecute(String result) {
            Log.d("", String.format("##### onPostExecute 結果 %s", result));
        }

        @Override
        // cancelで呼ばれる(任意)
        protected void onCancelled() {
            Log.d("","##### onCancelled");
        }

    }
}

以上

android ContentProviderで取得したvideoのパスを取得して再生する

今回やりたかったことは以下
・ギャラリーから動画を取得して
・その動画のパスを取得して
・その動画を再生

ギャラリー等の他のアプリからデータを取得する場合にContentProviderというクラスを使ってローカルDBからデータを取得する。検証したのはAndroid5以上。Android5未満のアプリはたぶん自分は手つけないので今回は考えない。

実装する必要があるのは以下の4つ
・ストレージへの読み込み権限追加
・ギャラリー起動
・ギャラリーからの戻り値でContentProviderを使ってファイルのパスを取得
・VideoViewで動画を再生

ストレージへの読み込み権限追加

AndroidManifest.xml

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
ギャラリー起動

ギャラリーを起動する

Intent intent = new Intent();
intent.setType("video/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent, "Select Video"), REQUEST_TAKE_GALLERY_VIDEO);
ContentProviderを使ってファイルのパスを取得

やってることはローカルDBから、「content://」形式のパスをキーに絶対パスを取得してる。今回は「MediaStore.MediaColumns.DATA」というカラムを指定してデータのパスを取得しているが例えば「MediaStore.MediaColumns.MIME_TYPE」とかやればデータのMimeTypeが取得したりもできる。「_id=?」とかの部分は検索条件指定している。

public void onActivityResult(int requestCode, int resultCode, Intent data) {

    if (resultCode != RESULT_OK) {
        return;
    } else if (requestCode != REQUEST_TAKE_GALLERY_VIDEO) {
        return;
    }

    // data.getData() : content://com.android.providers.media.documents/document/video%3A34

    // ファイルパス取得
    String strDocId = DocumentsContract.getDocumentId(data.getData());
    String[] strSplittedDocId = strDocId.split(":");
    String strId = strSplittedDocId[strSplittedDocId.length - 1];

    Cursor cursor = getContentResolver().query(
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
        new String[]{ MediaStore.MediaColumns.DATA },
        "_id=?",
        new String[]{strId},
        null
    );

    if (!cursor.moveToFirst()) {
        // failed
        cursor.close();
    } else {
        // success
        String inputPath = cursor.getString(0);
        cursor.close();

        // inputPath : /storage/emulated/0/DCIM/Camera/VID_20190313_214028.mp4
    }
}

ギャラリーから返ってきた
・「content://com.android.providers.media.documents/document/video%3A34」というキーを使って
・「/storage/emulated/0/DCIM/Camera/VID_20190313_214028.mp4」というパスを取得した

VideoViewで動画を再生
VideoView videoView =(VideoView)findViewById(R.id.videoView);
MediaController mediaController= new MediaController(MainActivity.this);
mediaController.setAnchorView(videoView);
videoView.setMediaController(mediaController);
// videoView.setVideoURI(data.getData());
videoView.setVideoPath(inputPath);
videoView.requestFocus();
videoView.start();

取得したパスをsetVideoPath()でセットしてstart()を呼べば動画が再生される。setVideoURI()を使えばContentProviderからファイルの絶対パスを取得しなくても良いのだが今回はファイルのパスの仕方を調べたかったのでせっかくなのでこうした...

まとめ

というか全体のコード

AndroidManifest.xml

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

MainActivity.java

package xxx.yyy.zzz

import android.content.Intent;
import android.database.Cursor;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.MediaController;
import android.widget.VideoView;

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_TAKE_GALLERY_VIDEO = 3;

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

        Intent intent = new Intent();
        intent.setType("video/*");
        intent.setAction(Intent.ACTION_GET_CONTENT);
        startActivityForResult(Intent.createChooser(intent, "Select Video"), REQUEST_TAKE_GALLERY_VIDEO);
    }

    public void onActivityResult(int requestCode, int resultCode, Intent data) {

        if (resultCode != RESULT_OK) {
            return;
        } else if (requestCode != REQUEST_TAKE_GALLERY_VIDEO) {
            return;
        }

        // ファイルパス取得
        String strDocId = DocumentsContract.getDocumentId(data.getData());
        String[] strSplittedDocId = strDocId.split(":");
        String strId = strSplittedDocId[strSplittedDocId.length - 1];

        Cursor crsCursor = getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.MediaColumns.DATA}
                , "_id=?", new String[]{strId}, null);

        if (!crsCursor.moveToFirst()) {
            crsCursor.close();
            return;
        }
        String inputPath = crsCursor.getString(0);
        crsCursor.close();
}

activity_main.xml

<VideoView android:id="@+id/videoView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

ちなみに実際のアプリ開発で利用する際は権限の許可をユーザーに求める処理も必要だが今回は試したかっただけなので書いてない。なのでAndroidの設定画面より等アプリのストレージ権限を追加しないと以下のようなエラーが出る。

java.lang.SecurityException: Permission Denial:
reading com.android.providers.media.MediaProvider uri content://media/external/video/media
from pid=9783, uid=10090 requires android.permission.READ_EXTERNAL_STORAGE, or grantUriPermission()

Kotolinに慣れようと思いつつググるJavaのコードばかり出てくるのでなかなかやれてない。時間がないと言いわけしつつ、、以上です