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) } }
クッキーの値を取得する
スワイプでブラウザバック
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)) } }
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に対応する
videoタグのフルスクリーンに対応する
全ソース
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を使えなくて、実際にアプリ開発で困ったこととその対策をまとめておく
最新のiOSのiPhoneにXcodeからアプリをインストールできない
iPhoneのiOSのバージョンアップを基本的に自動更新にしている人が多いと思うが、xcodeのバージョンは古いままだとxcodeからiPhoneにアプリをインストールできなくなる。その際に必要となるのが以下の記事の作業。Xcodeに含まれていないiOS Devise Supportファイルを手動で入れる作業が必要でその手順が以下に書かれている
旧バージョンのSwiftが使用できない
今回実際に困った例ではXcode10.1から10.2へのアップデートでSwiftのバージョンが4.2から5に変わっていたため。プロジェクトでCarthageを使用していてCarthageのコマンドでビルドしたライブラリはSwift5でビルドされる。しかしアプリはSwift4.2で開発したためSwift4.2でビルドしようとするとCarthageでビルドしたものとバージョンが異なるためビルドに失敗する。その際に必要となるのが以下の記事の作業。Xcodeのコマンドラインツールのバージョンを切り替える必要があり、その手順が以下に書かれている
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をダウンロードして使う。以下の記事に手順が書かれてる
今回はCircleCIでビルドしたアプリをAppStoreConnectへアップロードしたかったため、最新のSDKとversion.plistをgitに登録して、fastlaneのgymでビルドする際にsdkを指定する形で対応した
gym( ・・・ sdk: 'iOS 12.2', ・・・ )
【追記】
古いバージョンのXcodeでSDKだけ最新にしてもアプリ審査でリジェクトになる場合がある。やはり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のコードばかり出てくるのでなかなかやれてない。時間がないと言いわけしつつ、、以上です