公園、観光地などの案内図やバス時刻表のコピーなど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;
List list = 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() {
List listInform = 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:// に置き換えれば動いた。