公園、観光地などの案内図やバス時刻表のコピーなどpdfファイルや画像ファイルの名前には位置情報を含めている。 地図上にアイコンを表示し、このアイコンをタップすると、そのファイルを表示する。
タップの検出は写真撮影場所やバス停と同じため、行数が大きくなる場合には、共通化を図りたい。
Windows版ではファイル名は日本語でも問題はなかった。Androidエミュレータでは日本語ファイル名も正しく表示されるが、 スマホでは文字化けが起きる。 古くなっている情報も多い(特にバス時刻表で)ので、全面的に見直し、ファイル名は英字に変える。
*.pdf、*.mht ファイルの表示はWindows C#プログラムに比べて、はるかに面倒であることが分かった。
public class Information { final static String dirInfos = Map.DIR + "infos/"; static Information[] infos; double lon, lat; String path; char type; public Information(double lon, double lat, String path, char type) { this.lon = lon; this.lat = lat; this.path = path; this.type = type; } static void drawInfos(Map map, Canvas canvas) { if (!loadInfos()) return; Rect srcTime = new Rect(0, 0, Map.bmpTimetable.getWidth(), Map.bmpTimetable.getHeight()); Rect srcInfo = new Rect(0, 0, Map.bmpInformation.getWidth(), Map.bmpInformation.getHeight()); for (Information info : infos) { Bitmap bmp = info.type=='T' ? Map.bmpTimetable : Map.bmpInformation; Rect src = info.type=='T' ? srcTime : srcInfo; int x = (int)(Map.Lon2X(info.lon, map.zoom)*Map.PX - (map.CX - map.W/2f)); int y = (int)(Map.Lat2Y(info.lat, map.zoom)*Map.PX - (map.CY - map.H/2f)); if (x < 0 || x > map.W || y < 0 || y > map.H) continue; Rect dst = new Rect((x-18), (y-18), (x+18), (y+18)); canvas.drawBitmap(bmp, src, dst, null); } } public static boolean loadInfos() { if (infos != null) return true; // 読み込み済み String[] files = new File(dirInfos).list(); if (files == null) return false; Listlist = new ArrayList<>(); for (String file : files) { if (!file.endsWith(".pdf") && !file.endsWith(".mht") && !file.endsWith(".jpg") && !file.endsWith(".png") && !file.endsWith(".gif")) { continue; } if (file.length() < 20) continue; String str = null; try { str = file.substring(file.length() - 20, file.length() - 4); String[] lonlat = str.split("[_T]"); if (lonlat.length < 2) continue; double lon = Double.parseDouble(lonlat[0]); double lat = Double.parseDouble(lonlat[1]); list.add(new Information(lon, lat, file, str.charAt(8))); } catch (Exception ex) { System.out.printf("Error: %s[%s]\n", str, file); ex.printStackTrace(); } } infos = list.toArray(new Information[0]); return true; } }
// 一番近い情報アイコンを探す static Information getTappedInformation(double lon, double lat) { double minDistance = Double.MAX_VALUE; Information minInfo = null; for (Information info : infos) { double dx = info.lon - lon; double dy = info.lat - lat; double distance = Math.sqrt(dx * dx + dy * dy); if (distance < minDistance && distance < 0.1) { minDistance = distance; minInfo = info; } } return minInfo; } static void showInformation(double lon, double lat) { Information info = getTappedInformation(lon, lat); if (info == null) return; System.out.printf("%s\n", info.path); }
double xt = (eX + (CX - W / 2.0)) / PX; double yt = (eY + (CY - H / 2.0)) / PX; if (bus_route) { bus_route(xt, yt); } if (drawInformation) { double lon = X2Lon(xt, zoom); double lat = Y2Lat(yt, zoom); Information info = Information.getTappedInformation(lon, lat); if (info != null) { //String url = "file://" + Map.DIR + "infos/" + info.path; String url = "content://" + Map.DIR + "infos/" + info.path; Intent intent = new Intent(Intent.ACTION_VIEW); String extention = MimeTypeMap.getFileExtensionFromUrl(info.path); String mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extention); intent.setDataAndType(Uri.parse(url), mimetype); try { context.startActivity(intent); } catch (Exception ex) { ex.printStackTrace(); } } } }
class Information { const string Dir = "d:/gisdata/infos"; public string path; public double lon, lat; public int type; public Information(double lon, double lat, string path, int type) { this.lon = lon; this.lat = lat; this.path = path; this.type = type; } public static Information[] GetAllInformations() { ListlistInform = new List (); string[] files = Directory.GetFiles(Dir); foreach (string path in files) { if (path.Length < 20) continue; try { string str = path.Substring(path.Length-20,16); string[] lonlat = str.Split(new char []{'_', 'T'}); if (lonlat.Length < 2) continue; double lon = double.Parse(lonlat[0]); double lat = double.Parse(lonlat[1]); listInform.Add(new Information(lon,lat,path,str[8])); } catch { Console.Error.WriteLine(path); } } return listInform.ToArray(); } }
ファイル名は日本語の方が分かりやすい。LDPlayer(Android 9)では問題がないが、実機(Android 12)ではエラーが起きることが多い。 例えばファイル名は「相原駅西口1 大戸行き139.33150T35.60624.pdf」であり、位置情報はファイル名末尾にある。 file.substring(file.length() - 22, file.length() - 4); で 139.33150T35.60624 を切り出そうとすると、 実機では、表示でも文字化けがあるが、正しい切り出しが行えない。
表示上の文字化けは構わないが、末尾の位置情報だけを正しく切り出したい。substring や file.length() は使わず、 ファイル名の末尾から位置情報を切り出せればよい。
String を byte配列に変換して、位置情報部分を切り出し、その byte配列を String に戻せはうまく行くかもしれない。
byte[] sbyte = file.getBytes(StandardCharsets.ISO_8859_1); // Shift-JIS byte[] locbyte = Arrays.copyOfRange(sbyte, sbyte.length-22, sbyte.length-4); String loc = new String(locbyte);
しかし、これでもダメだった。誤りが常に先頭文字に現れるので、暫定的に、次のようにした。
if (str.charAt(0) != '1') str = "1" + str.substring(1); // 暫定
色んな文字に対して、'1' が 'T' に化けている。文字コード以外に何か問題があるのかもしれない。
java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/2EBA-4DDF/Map/infos/nice139.50**_35.54**.gif at androidx.core.content.FileProvider$SimplePathStrategy.getUriForFile(FileProvider.java:825) at androidx.core.content.FileProvider.getUriForFile(FileProvider.java:450) at com.example.mapx.Map.onTouchEvent(Map.java:583)
エミュレータでは次のようになった。
content://com.example.mapx.fileprovider/external_files/Map/infos/nice139.50**_35.54**.gif
エミュレータではファイルをメインストレージに置いており、実機では SDカードに置いている。この違いがエラーに関係しているかもしれない。
エミュレータでは "/storage/emulated/0/Map/"、 実機では "/storage/2EBA-4DDF/Map/" である。
external-pathは次のようにしている。 external-path はメインストレージを意味するのかもしれない。files はcom.example.mapxの files サブディレクトリであろう。
<external-path name="external_files" path="." />
実機の場合もファイルをメインストレージに置いた。これでエラーは取れた。SDカードを共有することも可能と思われるが、 当面は、情報ファイルをメインストレージに置くこととする。
Information info = Information.getTappedInformation(lon, lat); if (info != null) { int index = info.path.lastIndexOf('.'); String extention = info.path.substring(index + 1); String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extention); if (mime == null && extention.equals("mht")) mime = "multipart/related"; File file = new File(DirInfo + "infos/" + info.path); Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file); System.out.printf("%s\n%s\n", info.path, uri.toString()); Intent intent = new Intent(Intent.ACTION_VIEW); if (mime == null) intent.setData(uri); // このケースはない else intent.setDataAndType(uri, mime); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//2023.9.23 try { context.startActivity(intent); } catch (Exception ex) { ex.printStackTrace(); } }[xml/paths]
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <cache-path name="files" path="." /> <files-path name="document" path="document/" /> <external-path name="external_files" path="." /> </paths>[AndroidManifest.xml]
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/paths" /> </provider>
String url = "content://" + Map.DIR + "infos/" + Uri.encode(info.path);
System.out.printf("%s\n", url);
Intent intent = new Intent(Intent.ACTION_VIEW);
String extention = MimeTypeMap.getFileExtensionFromUrl(url);
String mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extention);
intent.setDataAndType(Uri.parse(url), mimetype);
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); context.startActivity(intent);で、次のエラーが起きた。
android.os.FileUriExposedException: file:///storage/emulated/0/Map/infos/jike139.4999_35.5664.pdf exposed beyond app through Intent.getData()
対策はネット検索で見つかった。
MainActivityの onCreate に以下を追加すればエラーは止まった。しかし、pdfファイルは開かない。ファイルマネージャでは開く。
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); StrictMode.setVmPolicy(builder.build());
Androidエミュレータでは 正常に表示されるようになった。しかし、スマホでは表示されない。
画像ファイルについてはエミュレータ、実機ともうまく表示されるようになった。pdfファイルについてはやはりエミュレータではうまくいくが、 実機ではダメ。
val target = Intent(Intent.ACTION_VIEW) val uri = FileProvider.getUriForFile(requireContext(), "${BuildConfig.APPLICATION_ID}.fileprovider", pdfFile) val mime = requireContext().contentResolver.getType(uri) target.setDataAndType(uri, mime) target.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION startActivity(target)
file:// を content:// に置き換えれば動いた。