diff --git a/android/res/layout/item_storage.xml b/android/res/layout/item_storage.xml index a3c592d67d..b563ca107a 100644 --- a/android/res/layout/item_storage.xml +++ b/android/res/layout/item_storage.xml @@ -6,4 +6,5 @@ android:layout_height="wrap_content" android:minHeight="@dimen/height_item_oneline" android:checkMark="?android:attr/listChoiceIndicatorSingle" + android:minLines="3" tools:text="/storage/mnt/"/> diff --git a/android/src/com/mapswithme/maps/routing/BaseRoutingErrorDialogFragment.java b/android/src/com/mapswithme/maps/routing/BaseRoutingErrorDialogFragment.java index e23f26c42c..0631444059 100644 --- a/android/src/com/mapswithme/maps/routing/BaseRoutingErrorDialogFragment.java +++ b/android/src/com/mapswithme/maps/routing/BaseRoutingErrorDialogFragment.java @@ -166,7 +166,7 @@ abstract class BaseRoutingErrorDialogFragment extends BaseMwmDialogFragment } Map group = new HashMap<>(); - group.put(GROUP_NAME, getString(R.string.maps) + " (" + mMissingMaps.size() + ") "); + group.put(GROUP_NAME, getString(R.string.downloader_status_maps) + " (" + mMissingMaps.size() + ") "); group.put(GROUP_SIZE, StringUtils.getFileSizeString(requireContext(), size)); List> groups = new ArrayList<>(); diff --git a/android/src/com/mapswithme/maps/settings/StorageItem.java b/android/src/com/mapswithme/maps/settings/StorageItem.java index 9729ca0170..fa80f6f655 100644 --- a/android/src/com/mapswithme/maps/settings/StorageItem.java +++ b/android/src/com/mapswithme/maps/settings/StorageItem.java @@ -9,11 +9,20 @@ public class StorageItem private final String mPath; // Free size. private final long mFreeSize; + // Total size. + private final long mTotalSize; + // User-visible description. + private final String mLabel; + // Is it read-only storage? + private final boolean mReadonly; - public StorageItem(String path, long size) + public StorageItem(String path, long freeSize, long totalSize, final String label, boolean isReadonly) { mPath = path; - mFreeSize = size; + mFreeSize = freeSize; + mTotalSize = totalSize; + mLabel = label; + mReadonly = isReadonly; } @Override @@ -37,12 +46,6 @@ public class StorageItem return 0; } - @Override - public String toString() - { - return mPath + ", " + mFreeSize; - } - public String getFullPath() { return mPath; @@ -52,4 +55,19 @@ public class StorageItem { return mFreeSize; } + + public long getTotalSize() + { + return mTotalSize; + } + + public String getLabel() + { + return mLabel; + } + + public boolean isReadonly() + { + return mReadonly; + } } diff --git a/android/src/com/mapswithme/maps/settings/StoragePathAdapter.java b/android/src/com/mapswithme/maps/settings/StoragePathAdapter.java index 573c51e5ae..8381c6fd31 100644 --- a/android/src/com/mapswithme/maps/settings/StoragePathAdapter.java +++ b/android/src/com/mapswithme/maps/settings/StoragePathAdapter.java @@ -1,17 +1,19 @@ package com.mapswithme.maps.settings; import android.app.Activity; -import android.view.LayoutInflater; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.format.Formatter; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.ForegroundColorSpan; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.CheckedTextView; -import androidx.annotation.NonNull; import com.mapswithme.maps.R; - -import java.util.ArrayList; -import java.util.List; +import com.mapswithme.util.ThemeUtils; +import com.mapswithme.util.UiUtils; class StoragePathAdapter extends BaseAdapter { @@ -50,10 +52,29 @@ class StoragePathAdapter extends BaseAdapter convertView = mActivity.getLayoutInflater().inflate(R.layout.item_storage, parent, false); StorageItem item = mPathManager.getStorageItems().get(position); + final boolean isCurrent = position == mPathManager.getCurrentStorageIndex(); CheckedTextView checkedView = (CheckedTextView) convertView; - checkedView.setText(item.getFullPath() + ": " + StoragePathFragment.getSizeString(item.getFreeSize())); - checkedView.setChecked(position == mPathManager.getCurrentStorageIndex()); - checkedView.setEnabled(position == mPathManager.getCurrentStorageIndex() || isStorageBigEnough(position)); + checkedView.setChecked(isCurrent); + checkedView.setEnabled(!item.isReadonly() && (isStorageBigEnough(position) || isCurrent)); + + final String size = mActivity.getString(R.string.maps_storage_free_size, + Formatter.formatShortFileSize(mActivity, item.getFreeSize()), + Formatter.formatShortFileSize(mActivity, item.getTotalSize())); + + SpannableStringBuilder sb = new SpannableStringBuilder(item.getLabel() + "\n" + size); + sb.setSpan(new ForegroundColorSpan(ThemeUtils.getColor(mActivity, android.R.attr.textColorSecondary)), + sb.length() - size.length(), sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + sb.setSpan(new AbsoluteSizeSpan(UiUtils.dimen(mActivity, R.dimen.text_size_body_3)), + sb.length() - size.length(), sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + final String path = item.getFullPath() + (item.isReadonly() ? " (read-only)" : ""); + sb.append("\n" + path); + sb.setSpan(new ForegroundColorSpan(ThemeUtils.getColor(mActivity, android.R.attr.textColorSecondary)), + sb.length() - path.length(), sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + sb.setSpan(new AbsoluteSizeSpan(UiUtils.dimen(mActivity, R.dimen.text_size_body_4)), + sb.length() - path.length(), sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + checkedView.setText(sb); return convertView; } diff --git a/android/src/com/mapswithme/maps/settings/StoragePathFragment.java b/android/src/com/mapswithme/maps/settings/StoragePathFragment.java index 5bba1c950d..a9193d71d4 100644 --- a/android/src/com/mapswithme/maps/settings/StoragePathFragment.java +++ b/android/src/com/mapswithme/maps/settings/StoragePathFragment.java @@ -5,8 +5,10 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckedTextView; import android.widget.ListView; import android.widget.TextView; +import android.text.format.Formatter; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; @@ -15,7 +17,6 @@ import com.mapswithme.maps.Framework; import com.mapswithme.maps.R; import com.mapswithme.maps.base.OnBackPressListener; import com.mapswithme.maps.dialog.DialogUtils; -import com.mapswithme.util.Constants; import com.mapswithme.util.StorageUtils; import com.mapswithme.util.Utils; import com.mapswithme.util.concurrency.ThreadPool; @@ -23,7 +24,6 @@ import com.mapswithme.util.concurrency.UiThread; import java.io.File; import java.util.List; -import java.util.Locale; public class StoragePathFragment extends BaseSettingsFragment implements OnBackPressListener @@ -60,7 +60,7 @@ public class StoragePathFragment extends BaseSettingsFragment { super.onResume(); mPathManager.startExternalStorageWatching((items, idx) -> updateList()); - mPathManager.updateExternalStorages(); + mPathManager.scanAvailableStorages(); updateList(); } @@ -88,7 +88,7 @@ public class StoragePathFragment extends BaseSettingsFragment private void updateList() { long dirSize = getWritableDirSize(); - mHeader.setText(getString(R.string.maps) + ": " + getSizeString(dirSize)); + mHeader.setText(getString(R.string.maps_storage_downloaded) + ": " + Formatter.formatShortFileSize(getActivity(), dirSize)); mAdapter.update(dirSize); } @@ -100,10 +100,11 @@ public class StoragePathFragment extends BaseSettingsFragment public void changeStorage(int newIndex) { final int currentIndex = mPathManager.getCurrentStorageIndex(); - if (newIndex == currentIndex || currentIndex == -1 || !mAdapter.isStorageBigEnough(newIndex)) + final List items = mPathManager.getStorageItems(); + if (newIndex == currentIndex || currentIndex == -1 || items.get(newIndex).isReadonly() + || !mAdapter.isStorageBigEnough(newIndex)) return; - final List items = mPathManager.getStorageItems(); final String oldPath = items.get(currentIndex).getFullPath(); final String newPath = items.get(newIndex).getFullPath(); @@ -135,38 +136,18 @@ public class StoragePathFragment extends BaseSettingsFragment if (!result) { - final String message = "Error moving maps files"; new AlertDialog.Builder(requireActivity()) - .setTitle(message) + .setTitle(R.string.move_maps_error) .setPositiveButton(R.string.report_a_bug, - (dlg, which) -> Utils.sendBugReport(requireActivity(), message)) + (dlg, which) -> Utils.sendBugReport(requireActivity(), "Error moving map files")) .show(); } - mPathManager.updateExternalStorages(); + mPathManager.scanAvailableStorages(); updateList(); }); }); } - static String getSizeString(long size) - { - final String[] units = { "Kb", "Mb", "Gb" }; - - long current = Constants.KB; - int i = 0; - for (; i < units.length; ++i) - { - final long bound = Constants.KB * current; - if (size < bound) - break; - - current = bound; - } - - // left 1 digit after the comma and add postfix string - return String.format(Locale.US, "%.1f %s", (double) size / current, units[i]); - } - @Override public boolean onBackPressed() { diff --git a/android/src/com/mapswithme/maps/settings/StoragePathManager.java b/android/src/com/mapswithme/maps/settings/StoragePathManager.java index 13349fe6d0..fe83dd8a00 100644 --- a/android/src/com/mapswithme/maps/settings/StoragePathManager.java +++ b/android/src/com/mapswithme/maps/settings/StoragePathManager.java @@ -7,11 +7,14 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Environment; +import android.os.storage.StorageManager; +import android.os.storage.StorageVolume; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.mapswithme.maps.Framework; +import com.mapswithme.maps.R; import com.mapswithme.maps.downloader.MapManager; import com.mapswithme.util.Config; import com.mapswithme.util.StorageUtils; @@ -20,13 +23,10 @@ import com.mapswithme.util.log.Logger; import com.mapswithme.util.log.LoggerFactory; import java.io.File; -import java.io.FileFilter; import java.io.FilenameFilter; import java.io.IOException; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; public class StoragePathManager { @@ -78,7 +78,7 @@ public class StoragePathManager @Override public void onReceive(Context context, Intent intent) { - updateExternalStorages(); + scanAvailableStorages(); if (mStoragesChangedListener != null) mStoragesChangedListener.onStorageListChanged(mItems, mCurrentStorageIndex); @@ -126,118 +126,165 @@ public class StoragePathManager } /** - * Updates the list of available external storages. - * - * The scan order is following: - * - * 1. Current directory from Config. - * 2. Application-specific directories on shared/external storage devices. - * 3. Application-specific directory on the internal memory. - * - * Directories are checked for the free space and the write access. + * Adds a storage into the list if it passes sanity checks. */ - public void updateExternalStorages() throws AssertionError + private void addStorageOption(File dir, boolean isInternal, String configPath) { - List candidates = new ArrayList<>(); - - // Current directory. - // Config.getStoragePath() can be empty on the first run. - String configDir = Config.getStoragePath(); - if (!TextUtils.isEmpty(configDir)) - candidates.add(new File(configDir)); - - // External storages (SD cards and other). - for (File dir : mContext.getExternalFilesDirs(null)) + // Internal storage must always exists, but Android is unpredictable. + // External storages can be null in some cases. + // https://github.com/organicmaps/organicmaps/issues/632 + if (dir == null) { - // There was an evidence that `dir` can be null on some Samsungs. - // https://github.com/organicmaps/organicmaps/issues/632 - if (dir == null) - continue; - - // - // If the contents of emulated storage devices are backed by a private user data partition, - // then there is little benefit to apps storing data here instead of the private directories - // returned by Context#getFilesDir(), etc. - // - boolean isStorageEmulated; - try - { - isStorageEmulated = Environment.isExternalStorageEmulated(dir); - } - catch (IllegalArgumentException e) - { - // isExternalStorageEmulated may throw IllegalArgumentException - // https://github.com/organicmaps/organicmaps/issues/538 - isStorageEmulated = false; - } - if (!isStorageEmulated) - candidates.add(dir); + LOGGER.w(TAG, "The system returned 'null' " + (isInternal ? "internal" : "external") + " storage"); + return; } - // Internal storage must always exists, but Android is unpredictable. - // https://github.com/organicmaps/organicmaps/issues/632 - File internalDir = mContext.getFilesDir(); - if (internalDir != null) - candidates.add(internalDir); + String commentedPath = null; + try + { + // Add the trailing separator because the native code assumes that all paths have it. + final String path = StorageUtils.addTrailingSeparator(dir.getCanonicalPath()); + final boolean isCurrent = path.equals(configPath); + final long totalSize = dir.getTotalSpace(); + final long freeSize = dir.getUsableSpace(); - if (candidates.isEmpty()) - throw new AssertionError("Can't find available storage"); + commentedPath = path + (StorageUtils.addTrailingSeparator(dir.getPath()).equals(path) + ? "" : " (" + dir.getPath() + ")") + " - " + + (isCurrent ? "currently configured, " : "") + + (isInternal ? "internal" : "external") + ", " + + freeSize + " available out of " + totalSize + " bytes"; + + boolean isEmulated = false; + boolean isRemovable = false; + boolean isReadonly = false; + String state = null; + String label = null; + if (!isInternal) + { + try + { + isEmulated = Environment.isExternalStorageEmulated(dir); + isRemovable = Environment.isExternalStorageRemovable(dir); + state = Environment.getExternalStorageState(dir); + commentedPath += (isEmulated ? ", emulated" : "") + + (isRemovable ? ", removable" : "") + + (state != null ? ", state=" + state : ""); + } + catch (IllegalArgumentException e) + { + // Thrown if the dir is not a valid storage device. + // https://github.com/organicmaps/organicmaps/issues/538 + LOGGER.w(TAG, "isExternalStorage checks failed for " + commentedPath); + } + + // Get additional storage information for Android 7+. + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) + { + final StorageManager sm = (StorageManager) mContext.getSystemService(mContext.STORAGE_SERVICE); + if (sm != null) + { + final StorageVolume sv = sm.getStorageVolume(dir); + if (sv != null) + { + label = sv.getDescription(mContext); + commentedPath += (sv.isPrimary() ? ", primary" : "") + + (!TextUtils.isEmpty(sv.getUuid()) ? ", uuid=" + sv.getUuid() : "") + + (!TextUtils.isEmpty(label) ? ", label='" + label + "'": ""); + } + else + LOGGER.w(TAG, "Can't get StorageVolume for " + commentedPath); + } + else + LOGGER.w(TAG, "Can't get StorageManager for " + commentedPath); + } + } + + if (state != null && !Environment.MEDIA_MOUNTED.equals(state) + && !Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) + { + LOGGER.w(TAG, "Not mounted: " + commentedPath); + return; + } + if (!dir.exists()) + { + LOGGER.w(TAG, "Not exists: " + commentedPath); + return; + } + if (!dir.isDirectory()) + { + LOGGER.w(TAG, "Not a directory: " + commentedPath); + return; + } + if (!dir.canWrite() || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) + { + isReadonly = true; + LOGGER.w(TAG, "Not writable: " + commentedPath); + // Keep using currently configured storage even if its read-only. + if (isCurrent) + commentedPath += ", read-only"; + else + return; + } + if (isEmulated) + { + // TODO: External emulated storage can be backed either by internal or adopted external storage. + // https://github.com/organicmaps/organicmaps/issues/2451 + LOGGER.i(TAG, "Emulated storage: " + commentedPath); + return; + } + + if (TextUtils.isEmpty(label)) + label = isInternal ? mContext.getString(R.string.maps_storage_internal) + : (isRemovable ? mContext.getString(R.string.maps_storage_removable) + : (isEmulated ? mContext.getString(R.string.maps_storage_shared) + : mContext.getString(R.string.maps_storage_external))); + + StorageItem item = new StorageItem(path, freeSize, totalSize, label, isReadonly); + mItems.add(item); + if (isCurrent) + mCurrentStorageIndex = mItems.size() - 1; + LOGGER.i(TAG, "Accepted " + commentedPath); + } + catch (SecurityException | IOException ex) + { + LOGGER.e(TAG, "Error: " + (commentedPath != null ? commentedPath : "(" + dir.getPath() + ")"), ex); + } + } + + /** + * Updates the list of available storages. + * The scan order is following: + * 1. App-specific directories on shared/external storage devices. + * 2. App-specific directory in the internal memory. + */ + public void scanAvailableStorages() throws AssertionError + { + // Current configured storage directory, can be empty on the first run. + final String configPath = Config.getStoragePath(); + LOGGER.i(TAG, "Currently configured storage: " + (TextUtils.isEmpty(configPath) ? "N/A" : configPath)); - // - // Update internal state. - // LOGGER.i(TAG, "Begin scanning storages"); mItems.clear(); mCurrentStorageIndex = -1; - Set unique = new HashSet<>(); - for (File dir : candidates) + + // External storages (SD cards and other). + for (File externalDir : mContext.getExternalFilesDirs(null)) { - try - { - String path = dir.getCanonicalPath(); - // Add the trailing separator because the native code assumes that all paths have it. - if (!path.endsWith(File.separator)) - path = path + File.separator; - - if (!dir.exists() || !dir.isDirectory()) - { - LOGGER.i(TAG, "Rejected " + path + ": not a directory"); - continue; - } - if (!dir.canWrite()) - { - LOGGER.i(TAG, "Rejected " + path + ": not writable"); - continue; - } - - if (!unique.add(path)) - { - LOGGER.i(TAG, "Rejected " + path + ": a duplicate"); - continue; - } - - final long freeSize = StorageUtils.getFreeBytesAtPath(path); - StorageItem item = new StorageItem(path, freeSize); - if (!TextUtils.isEmpty(configDir) && configDir.equals(path)) - { - mCurrentStorageIndex = mItems.size(); - } - LOGGER.i(TAG, "Accepted " + path + ": " + freeSize + " bytes available"); - mItems.add(item); - } - catch (IllegalArgumentException | IOException ex) - { - LOGGER.e(TAG, "Rejected " + dir.getPath() + ": error", ex); - continue; - } + addStorageOption(externalDir, false, configPath); } + + File internalDir = mContext.getFilesDir(); + addStorageOption(internalDir, true, configPath); + LOGGER.i(TAG, "End scanning storages"); - if (!TextUtils.isEmpty(configDir) && mCurrentStorageIndex == -1) + if (mItems.isEmpty()) + // Shut down the app. + throw new AssertionError("Can't find available storages"); + + if (!TextUtils.isEmpty(configPath) && mCurrentStorageIndex == -1) { - LOGGER.w(TAG, configDir + ": can't find configured directory in the list above!"); - for (StorageItem item : mItems) - LOGGER.w(TAG, item.toString()); + LOGGER.w(TAG, "Currently configured storage is not available!"); } } @@ -254,62 +301,79 @@ public class StoragePathManager private static boolean containsMapData(String storagePath) { File path = new File(storagePath); - File[] candidates = path.listFiles(new FileFilter() - { - @Override - public boolean accept(File pathname) + File[] candidates = path.listFiles((pathname) -> { - if (!pathname.isDirectory()) - return false; + if (!pathname.isDirectory()) + return false; - try - { - String name = pathname.getName(); - if (name.length() != 6) - return false; + try + { + String name = pathname.getName(); + if (name.length() != 6) + return false; - int version = Integer.valueOf(name); - return (version > 120000 && version <= 999999); - } catch (NumberFormatException ignored) {} + int version = Integer.valueOf(name); + return (version > 120000 && version <= 999999); + } + catch (NumberFormatException ignored) + { + } - return false; - } - }); + return false; + }); return (candidates != null && candidates.length > 0 && candidates[0].list().length > 0); } /** - * Finds a first available storage with existing maps files. - * Returns the best available option if no maps files found. - * See updateExternalStorages() for the scan order details. + * Returns an available storage with existing maps files. + * Checks the currently configured storage first, + * then scans other storages. Defaults to the first available option if no maps files found + * (see scanAvailableStorages() for the scan order details). */ public static String findMapsStorage(@NonNull Application application) { - StoragePathManager instance = new StoragePathManager(application); - instance.updateExternalStorages(); + StoragePathManager mgr = new StoragePathManager(application); + mgr.scanAvailableStorages(); + String path = null; + final int currentIdx = mgr.getCurrentStorageIndex(); - List items = instance.getStorageItems(); - - for (StorageItem item : items) + if (currentIdx != -1) { - String path = item.getFullPath(); + path = mgr.getStorageItems().get(currentIdx).getFullPath(); if (containsMapData(path)) { - LOGGER.i(TAG, "Found maps files at " + path); + LOGGER.i(TAG, "Found map files at the currently configured " + path); return path; } else { - LOGGER.i(TAG, "No maps files found at " + path); + LOGGER.w(TAG, "No map files found at the currenly configured " + path); } } - // Use the first item by default. - final String defaultDir = items.get(0).getFullPath(); - LOGGER.i(TAG, "Using default directory: " + defaultDir); - return defaultDir; + LOGGER.i(TAG, "Looking for map files in available storages..."); + for (int idx = 0; idx < mgr.getStorageItems().size(); ++idx) + { + if (idx == currentIdx) + continue; + path = mgr.getStorageItems().get(idx).getFullPath(); + if (containsMapData(path)) + { + LOGGER.i(TAG, "Found map files at " + path); + return path; + } + else + { + LOGGER.i(TAG, "No map files found at " + path); + } + } + + // Use the first storage by default. + path = mgr.getStorageItems().get(0).getFullPath(); + LOGGER.i(TAG, "Using default storage: " + path); + return path; } /** diff --git a/android/src/com/mapswithme/util/StorageUtils.java b/android/src/com/mapswithme/util/StorageUtils.java index 3441c760d7..f063c16a04 100644 --- a/android/src/com/mapswithme/util/StorageUtils.java +++ b/android/src/com/mapswithme/util/StorageUtils.java @@ -123,9 +123,9 @@ public class StorageUtils } @NonNull - private static String addTrailingSeparator(@NonNull String dir) + public static String addTrailingSeparator(@NonNull String dir) { - if (!dir.endsWith("/")) + if (!dir.endsWith(File.separator)) return dir + File.separator; return dir; } @@ -250,20 +250,6 @@ public class StorageUtils } } - public static long getFreeBytesAtPath(String path) - { - long size = 0; - try - { - size = new File(path).getFreeSpace(); - } catch (RuntimeException e) - { - e.printStackTrace(); - } - - return size; - } - public static long getDirSizeRecursively(File file, FilenameFilter fileFilter) { if (file.isDirectory()) diff --git a/data/strings/strings.txt b/data/strings/strings.txt index bbbf809de9..233265ca3f 100644 --- a/data/strings/strings.txt +++ b/data/strings/strings.txt @@ -342,44 +342,6 @@ zh-Hans = 发表评论 zh-Hant = 撰寫評論 - [maps] - comment = View and button titles for accessibility - tags = android,ios - en = Maps - ar = الخرائط - be = Мапы - bg = Карти - cs = Mapy - da = Kort - de = Karten - el = Χάρτες - es = Mapas - eu = Mapak - fa = نقشه ها - fi = Kartat - fr = Cartes - he = מפות - hu = Térképek - id = Peta - it = Mappe - ja = マップ - ko = 지도 - nb = Kart - nl = Kaarten - pl = Mapy - pt = Mapas - pt-BR = Mapas - ro = Hărţi - ru = Карты - sk = Mapy - sv = Kartor - th = แผนที่ - tr = Haritalar - uk = Мапи - vi = Bản đồ - zh-Hans = 地图 - zh-Hant = 地圖 - [mb] comment = Settings/Downloader - size string, only strings different from English should be translated tags = android @@ -1835,6 +1797,80 @@ zh-Hans = 选取地图应该下载的位置 zh-Hant = 請選擇下載地圖後要放的位置 + [maps_storage_downloaded] + comment = E.g. "Downloaded maps: 500Mb" in Maps Storage settings + tags = android + en = Downloaded maps + ar = الخرائط + be = Мапы + bg = Карти + cs = Mapy + da = Kort + de = Karten + el = Χάρτες + es = Mapas + eu = Mapak + fa = نقشه ها + fi = Kartat + fr = Cartes + he = מפות + hu = Térképek + id = Peta + it = Mappe + ja = マップ + ko = 지도 + nb = Kart + nl = Kaarten + pl = Mapy + pt = Mapas + pt-BR = Mapas + ro = Hărţi + ru = Загруженные карты + sk = Mapy + sv = Kartor + th = แผนที่ + tr = Haritalar + uk = Завантаженнi мапи + vi = Bản đồ + zh-Hans = 地图 + zh-Hant = 地圖 + + [maps_storage_internal] + comment = Internal storage type in Maps Storage settings (not accessible by the user) + tags = android + en = Internal hidden storage + ru = Внутренний скрытый накопитель + uk = Внутрішнє зховане сховище + + [maps_storage_shared] + comment = Shared storage type in Maps Storage settings (a primary storage usually) + tags = android + en = Internal shared storage + be = Унутранае абагуленае сховішча + ru = Внутренний общий накопитель + uk = Внутрішнє спільне сховище + + [maps_storage_removable] + comment = Removable external storage type in Maps Storage settings, e.g. an SD card + tags = android + en = SD card + be = SD-карта + ru = SD-карта + uk = SD-карта + + [maps_storage_external] + comment = Generic external storage type in Maps Storage settings + tags = android + en = External shared storage + ru = Внешний общий накопитель + uk = Зовнiшне спільне сховище + + [maps_storage_free_size] + comment = Free space out of total storage size in Maps Storage settings, e.g. "300 MB free (of 2 GB)" + tags = android + en = %1$@ free (of %2$@) + ru = %1$@ свободно (из %2$@) + [move_maps] comment = Question dialog for transferring maps from one storage to another tags = android @@ -1873,6 +1909,12 @@ zh-Hans = 移动地图? zh-Hant = 移動地圖? + [move_maps_error] + comment = Error moving map files from one storage to another + tags = android + en = Error moving map files + ru = Ошибка перемещения файлов карт + [wait_several_minutes] comment = Ask to wait user several minutes (some long process in modal dialog). tags = android @@ -5228,7 +5270,7 @@ zh-Hant = 全部取消 [downloader_downloaded_subtitle] - comment = Downloaded maps category + comment = Downloaded maps list header tags = android,ios en = Downloaded ar = تم تنزيلها @@ -5375,6 +5417,7 @@ zh-Hant = 在我附近 [downloader_status_maps] + comment = In maps downloader and country place page shows how many maps are downloaded / to download, e.g. "Maps: 3 of 10" tags = android,ios en = Maps ar = الخرائط diff --git a/iphone/Maps/Classes/CustomAlert/DownloadTransitMapsAlert/MWMDownloadTransitMapAlert.mm b/iphone/Maps/Classes/CustomAlert/DownloadTransitMapsAlert/MWMDownloadTransitMapAlert.mm index e81e19b7cf..519086345d 100644 --- a/iphone/Maps/Classes/CustomAlert/DownloadTransitMapsAlert/MWMDownloadTransitMapAlert.mm +++ b/iphone/Maps/Classes/CustomAlert/DownloadTransitMapsAlert/MWMDownloadTransitMapAlert.mm @@ -334,7 +334,7 @@ CGFloat const kAnimationDuration = .05; if (!_listHeader) _listHeader = [MWMDownloaderDialogHeader headerForOwnerAlert:self]; - [_listHeader setTitle:[NSString stringWithFormat:@"%@ (%@)", L(@"maps"), @(m_countries.size())] + [_listHeader setTitle:[NSString stringWithFormat:@"%@ (%@)", L(@"downloader_status_maps"), @(m_countries.size())] size:self.countriesSize]; return _listHeader; }