public class DownloadFiles {
    private static DownloadFiles downloadFiles;

    private DownloadFiles() {
    }

    public static DownloadFiles getDownloader() {
        if (downloadFiles == null) {
            downloadFiles = new DownloadFiles();
        }
        return downloadFiles;
    }
    
  /**
   * @param mapUrl
   * @return json string
   */
  public String downloadTextfile(String textFileUrl)
  {
    StringBuilder json = new StringBuilder();
    try (BufferedReader in = new BufferedReader(new InputStreamReader(new URL(textFileUrl).openStream())))
    {
      String lineUrl;
      android.util.Log.i(DownloadFiles.class.getName(),"Pre stream dl");
      while ((lineUrl = in.readLine()) != null)
      {
        json.append(lineUrl);
      }
      in.close();
      android.util.Log.i(DownloadFiles.class.getName(),"Close stream dl");
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
    return json.toString();
  }

} public class MapDownloadUnzip
{
  private static Thread unzipThread;
  private static List<MyMap> unzipMapQueue = java.util.Collections.synchronizedList(new ArrayList<MyMap>());
  
  public static void checkMap(Activity activity, MyMap tmpMap, StatusUpdate stUpdate)
  {
    File idFile = MyMap.getMapFile(tmpMap, MyMap.MapFileType.DlIdFile);
    if (!idFile.exists())
    {
      stUpdate.logUserThread("Unzipping: " + tmpMap.getMapName());
      unzipBg(activity, tmpMap, stUpdate);
      return;
    }
    String idFileContent = IO.readFromFile(idFile, "\n");
    if (idFileContent.startsWith("" + MyMap.DlStatus.Error + ": "))
    {
      stUpdate.logUserThread(idFileContent);
      DownloadMapActivity.clearDlFile(tmpMap);
    }
    else if (!idFileContent.replace("\n", "").isEmpty())
    {
      int id = Integer.parseInt(idFileContent.replace("\n", ""));
      broadcastReceiverCheck(activity, tmpMap, stUpdate, id);
    }
  }
  
  private static boolean isUnzipQueue(MyMap map)
  {
    return unzipMapQueue.contains(map) && unzipThread!=null && unzipThread.isAlive();
  }
  
  public static void unzipBg(final Activity activity, final MyMap myMapForQueue, final StatusUpdate stUpdate)
  {
    if (isUnzipQueue(myMapForQueue) || MapUnzip.checkUnzipAlive(activity.getApplicationContext(), myMapForQueue))
    {
      log("Unzip is still in progress. Dont start twice.");
      return;
    }
    
    log("Unzipping map: " + myMapForQueue.getMapName());
    myMapForQueue.setStatus(MyMap.DlStatus.Unzipping);
    stUpdate.updateMapStatus(myMapForQueue);
    
    if (!unzipMapQueue.contains(myMapForQueue)) { unzipMapQueue.add(myMapForQueue); }
    
    if (unzipThread!=null && unzipThread.isAlive())
    {
      return;
    }
    
    Thread t = new Thread(new Runnable(){ public void run()
    { // Because this may be a long running task, we dont use AsyncTask.
      while(!unzipMapQueue.isEmpty())
      {
        final MyMap myMap = unzipMapQueue.remove(0);
        String errMsg = null;
        Context c = activity.getApplicationContext();
        String unzipKeyId = myMap.getMapName() + "-unzip";
        ProgressPublisher pp = new ProgressPublisher(c, unzipKeyId.hashCode());
        pp.updateText(false, "Unzipping " + myMap.getMapName(), 0);
        File ghzFile = MyMap.getMapFile(myMap, MyMap.MapFileType.DlMapFile);
        if (ghzFile.exists())
        {
          try
          {
            new MapUnzip().unzip(ghzFile.getPath(), myMap.getMapName(), pp);
          }
          catch (IOException e)
          {
            errMsg = "Error unpacking map: " + myMap.getMapName();
          }
        }
        else
        {
          errMsg = "Error, missing downloaded file: " + ghzFile.getPath();
        }
        if (errMsg!=null)
        {
          pp.updateTextFinal(errMsg);
        }
        else
        {
          pp.updateTextFinal("Extracting finished: " + myMap.getMapName());
        }
        final String errMsgFinal = errMsg;
        DownloadMapActivity.clearDlFile(myMap);
        activity.runOnUiThread(new Runnable() { public void run()
        {
          if (errMsgFinal!=null)
          {
            File idFile = MyMap.getMapFile(myMap, MyMap.MapFileType.DlIdFile);
            IO.writeToFile("" + MyMap.DlStatus.Error + ": " + errMsgFinal, idFile, false);
            myMap.setStatus(MyMap.DlStatus.Error);
            stUpdate.updateMapStatus(myMap);
            return;
          }
          Variable.getVariable().getRecentDownloadedMaps().add(myMap);
          MyMap.setVersionCompatible(myMap.getMapName(), myMap);
          myMap.setStatus(MyMap.DlStatus.Complete);
          stUpdate.updateMapStatus(myMap);
        } });
      }
    } });
    t.start();
    unzipThread = t;
  }

  /** Check first, if map is finished, or on pending status register receiver. **/
  private static void broadcastReceiverCheck(Activity activity,
                                       final MyMap myMap,
                                       final StatusUpdate stUpdate,
                                       final long enqueueId)
  {
    int preStatus = getDownloadStatus(activity, enqueueId);
    if (preStatus == DownloadManager.STATUS_SUCCESSFUL)
    {
      stUpdate.logUserThread("Unzipping: " + myMap.getMapName());
      unzipBg(activity, myMap, stUpdate);
      return;
    }
    else if (preStatus == DownloadManager.STATUS_FAILED)
    {
      DownloadMapActivity.clearDlFile(myMap);
      stUpdate.logUserThread("Error post-downloading map: " + myMap.getMapName());
    }
    else
    {
      stUpdate.onRegisterBroadcastReceiver(activity, myMap, enqueueId);
    }
  }
  
  public static int getDownloadStatus(Context context, long enqueueId)
  {
    Query query = new Query();
    DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
    query.setFilterById(enqueueId);
    Cursor c = dm.query(query);
    if (c.moveToFirst())
    {
      int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
      return c.getInt(columnIndex);
    }
    return -1; // Aborted.
  }
  
  public static interface StatusUpdate
  {
    public void logUserThread(String txt);
    public void updateMapStatus(MyMap map);
    public void onRegisterBroadcastReceiver(Activity activity, MyMap myMap, long enqueueId);
  }
  
  private static void log(String s) {
    Log.i(MapDownloadUnzip.class.getName(), s);
  }
} public static interface StatusUpdate
{
  public void logUserThread(String txt);
  public void updateMapStatus(MyMap map);
  public void onRegisterBroadcastReceiver(Activity activity, MyMap myMap, long enqueueId);
} public class MyMapAdapter extends RecyclerView.Adapter<MyMapAdapter.ViewHolder> {
    private List<MyMap> myMaps;
    private List<MyMap> myMapsFiltered;
    private OnClickMapListener onClickMapListener;
    private boolean isDownloadingView;

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public FloatingActionButton flag;
        public TextView name, continent, size, downloadStatus;
        public OnClickMapListener onClickMapListener;
        private boolean isDownloadingView;

        protected ViewHolder(View itemView, OnClickMapListener onClickMapListener, boolean isDownloadingView) {
            super(itemView);
            this.isDownloadingView = isDownloadingView;
            this.onClickMapListener = onClickMapListener;
            this.flag = (FloatingActionButton) itemView.findViewById(R.id.my_maps_item_flag);
            this.name = (TextView) itemView.findViewById(R.id.my_maps_item_name);
            this.continent = (TextView) itemView.findViewById(R.id.my_maps_item_continent);
            this.size = (TextView) itemView.findViewById(R.id.my_maps_item_size);
            this.downloadStatus = (TextView) itemView.findViewById(R.id.my_maps_item_download_status);
        }

        public void setItemData(MyMap myMap) {
            name.setTextColor(android.graphics.Color.BLACK);
            if (isDownloadingView)
            {
                MyMap.DlStatus status = myMap.getStatus();

                if (status == MyMap.DlStatus.Downloading)
                {
                    flag.setImageResource(R.drawable.ic_pause_orange_24dp);
                    downloadStatus.setText("Downloading ...");
                }
                else if (status == MyMap.DlStatus.Unzipping)
                {
                  flag.setImageResource(R.drawable.ic_pause_orange_24dp);
                  downloadStatus.setText("Unzipping ...");
                }
                else if (status == MyMap.DlStatus.Complete)
                {
                  if (myMap.isUpdateAvailable())
                  {
                     flag.setImageResource(R.drawable.ic_cloud_download_white_24dp);
                     name.setTextColor(android.graphics.Color.RED);
                  }
                  else
                  {
                    flag.setImageResource(R.drawable.ic_map_white_24dp);
                  }
                  downloadStatus.setText("Downloaded");
                }
                else
                {
                  flag.setImageResource(R.drawable.ic_cloud_download_white_24dp);
                  downloadStatus.setText("");
                }
            }
            else
            {
                downloadStatus.setText("");
            }

            View.OnClickListener l = new View.OnClickListener()
            {
                public void onClick(View v)
                {
                    log("onclick" + itemView.toString());
                    onClickMapListener.onClickMap(ViewHolder.this.getAdapterPosition());
                }
            };
            name.setText(myMap.getCountry());
            continent.setText(myMap.getContinent());
            size.setText(myMap.getSize());
            itemView.setOnClickListener(l);
        }

    }

    // Provide a suitable constructor (depends on the kind of dataset)
    public MyMapAdapter(List<MyMap> myMaps, OnClickMapListener onClickMapListener, boolean isDownloadingView)
    {
      this.myMaps = myMaps;
      this.myMapsFiltered = myMaps;
      this.onClickMapListener = onClickMapListener;
      this.isDownloadingView = isDownloadingView;
    }

    // Create new views (invoked by the layout manager)
    @Override
    public MyMapAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.my_maps_item, parent, false);
        ViewHolder vh = new ViewHolder(v, onClickMapListener, isDownloadingView);
        return vh;
    }

    // Replace the contents of a view (invoked by the layout manager)
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.setItemData(myMapsFiltered.get(position));
    }

    // Return the size of your dataset (invoked by the layout manager)
    @Override
    public int getItemCount() {
        return myMapsFiltered.size();
    }
    
    public void refreshMapView(MyMap myMap)
    {
      int rvIndex = myMapsFiltered.indexOf(myMap);
      if (rvIndex >= 0)
      {
        notifyItemRemoved(rvIndex);
        notifyItemInserted(rvIndex);
      }
      else
      {
        log("No map-entry for refreshing found, maybe filter active.");
      }
    }

    /**
     * @param position
     * @return MyMap item at the position
     */
    public MyMap getItem(int position) {
        return myMapsFiltered.get(position);
    }

    /**
     * remove item at the given position
     *
     * @param position index
     */
    public MyMap remove(int position) {
        MyMap mm = null;
        if (position >= 0 && position < getItemCount())
        {
          mm = myMaps.remove(position);
          int posFiltered = myMapsFiltered.indexOf(mm);
          if (posFiltered >= 0)
          { // Filter is active!
            myMapsFiltered.remove(posFiltered);
            position = posFiltered;
          }
          notifyItemRemoved(position);
        }
        return mm;
    }

    /**
     * Clear the list (remove all elements)
     * Does NOT call notifyItemRangeRemoved()
     */
    public void clearList() {
        this.myMaps.clear();
        this.myMapsFiltered.clear();
    }

    /**
     * add a list of MyMap
     *
     * @param maps
     */
    public void addAll(List<MyMap> maps) {
        this.myMaps.addAll(maps);
        if (myMaps == myMapsFiltered)
        {
          notifyItemRangeInserted(myMaps.size() - maps.size(), maps.size());
        }
    }

    /**
     * Insert the object to the end of the list
     * Executes notifyItemInserted()
     * @param myMap
     */
    public void insert(MyMap myMap) {
        if (!getMapNameList().contains(myMap.getMapName())) {
            myMaps.add(myMap);
            if (myMaps == myMapsFiltered)
            {
              notifyItemInserted(getItemCount() - 1);
            }
        }
    }


    /**
     * @return a string list of map names (continent_country)
     */
    public List<String> getMapNameList() {
        ArrayList<String> al = new ArrayList<String>();
        for (MyMap mm : myMaps) {
            al.add(mm.getMapName());
        }
        return al;
    }
    
    static void log(String txt)
    {
      Log.i(MyMapAdapter.class.getName(), txt);
    }

    public void doFilter(String filterText)
    {
      log("FILTER-START!");
      filterText = filterText.toLowerCase();
      List<MyMap> filteredList = new ArrayList<MyMap>();
      if (filterText.isEmpty())
      {
        filteredList = myMaps;
        log("FILTER: Empty");
      }
      else
      {
        for (MyMap curMap : myMaps)
        {
          if (curMap.getCountry().toLowerCase().contains(filterText) || curMap.getContinent().toLowerCase().contains(filterText))
          {
            filteredList.add(curMap);
          }
        }
        log("FILTER: " + filteredList.size() + "/" + myMaps.size());
      }
      myMapsFiltered = filteredList;
      notifyDataSetChanged();
      log("FILTER: Publish: " + myMapsFiltered.size() + "/" + myMaps.size());
    }
} public class MyMap implements Comparable<MyMap> {
    public enum MapFileType {DlMapFile, MapFolder, DlIdFile};
    public enum DlStatus {Downloading, Unzipping, Complete, On_server, Error};
    public static final String MAP_VERSION = "0.13.0_0"; // graphhopperVers_counterFlag
    private String country = "";
    private String size = "";
    private String url = "";
    private String continent = "";
    private String mapName = "";
    private String mapNameNew = "";
    private String timeRemote = "";
    private String timeLocal = "";
    private int resId = 0;
    private DlStatus status;
    private boolean updateAvailable = false;
    private long lastCheckTime = 0;

    /**
     * generate MyMap for local select list
     *
     * @param mapName map name
     */
    public MyMap(String mapName) {
        this.status = DlStatus.Complete;
        int index = mapName.indexOf("-gh");
        if (index > 0) {
            mapName = mapName.substring(0, index);
        }
        this.mapName = mapName;
        generateContinentName(mapName);
        File file = new File(Variable.getVariable().getMapsFolder().getAbsolutePath(), mapName + "-gh");
        setUrl(file.getAbsolutePath());
        long lSize = IO.dirSize(file);
        if (lSize>1000)
        {
          lSize = lSize/100;
          double gbSize = lSize/10.0;
          setSize(gbSize + "G");
        }
        else
        {
          setSize(IO.dirSize(file) + "M");
        }
    }
    
    private static File getVersionFile(String mapName)
    {
      char sep = File.separatorChar;
      String mapsFolder = Variable.getVariable().getMapsFolder().getAbsolutePath();
      File versFile = new File(mapsFolder, mapName + "-gh" + sep + "version.txt");
      return versFile;
    }
    
    public static boolean isVersionCompatible(String mapName)
    {
      File versFile = getVersionFile(mapName);
      String mapVers = null;
      if (versFile.exists())
      {
        mapVers = IO.readFromFile(versFile, "\n");
      }
      if (mapVers == null) { mapVers = "0"; }
      return mapVers.startsWith(MAP_VERSION + "\n");
    }
    
    /** DownloadFinished: Set the version same as remote-version, and as compatible version. **/
    public static boolean setVersionCompatible(String mapName, MyMap myMap)
    {
      File versFile = getVersionFile(mapName);
      myMap.updateAvailable = false;
      boolean testUpdateAvailable = (myMap.getTime(true).compareTo(myMap.getTime(false))>0);
      if (testUpdateAvailable)
      {
        myMap.timeLocal = myMap.timeRemote;
      }
      return IO.writeToFile(MAP_VERSION + "\n" + myMap.getTime(true), versFile, false);
    }
    
    public static boolean setVersionIncompatible(String mapName, MyMap myMap)
    {
      File versFile = getVersionFile(mapName);
      if (versFile.exists()) { return versFile.delete(); }
      return true;
    }
    
    public static File getMapFile(MyMap myMap, MapFileType fileType)
    {
      File dlFolder = Variable.getVariable().getDownloadsFolder();
      if (fileType == MapFileType.DlMapFile)
      {
        return new File(dlFolder, myMap.getMapName() + ".ghz");
      }
      else if (fileType == MapFileType.DlIdFile)
      {
        return new File(dlFolder, myMap.getMapName() + ".id");
      }
      else // MapFolder
      {
        return getVersionFile(myMap.getMapName()).getParentFile();
      }
    }
    
    private long getLastCheckTimeSpan()
    {
      return System.currentTimeMillis() - lastCheckTime;
    }
    /**
     * generate MyMap for download list
     *
     * @param mapName map name
     * @param size    map size
     */
    public MyMap(String mapName, String size, String timeRemote, String mapUrl) {
        this.mapName = mapName;
        this.size = size;
        this.timeRemote = timeRemote;
        initStatus();
        setUrl(mapUrl + mapName + ".ghz");
        generateContinentName(mapName);
    }
    
    public void set(MyMap otherMap)
    {
      this.mapName = otherMap.mapName;
      this.mapNameNew = otherMap.mapNameNew;
      this.size = otherMap.size;
      this.timeRemote = otherMap.timeRemote;
      this.status = otherMap.status;
      this.url = otherMap.url;
      this.continent = otherMap.continent;
    }

    private void initStatus()
    {
      if (Variable.getVariable().getLocalMapNameList().contains(mapName))
      {
        status = DlStatus.Complete;
      }
      else
      {
        status = DlStatus.On_server;
      }
    }

    /**
     * split map name to continent name and name and upper case first letter
     *
     * @param mapName map name
     */
    private void generateContinentName(String mapName) {
        String[] s = mapName.split("_");
        setContinent(Character.toString(s[0].charAt(0)).toUpperCase() + s[0].substring(1));
        String country = "";
        for (int i = 1; i < s.length; i++) {
            country += Character.toString(s[i].charAt(0)).toUpperCase() + s[i].substring(1) + " ";
        }
        setCountry(country.substring(0, country.length() - 1));
    }
    
    /** Compare mapTimeLocal and mapTimeRemote.
     *  <br/>Reads mapTimeLocal on first time. **/
    public boolean isUpdateAvailable()
    {
      if (updateAvailable) { return true; }
      log("GH Compare maps Local=" + getTime(false) + " Remote=" + getTime(true));
      updateAvailable = (getTime(true).compareTo(getTime(false))>0);
      return updateAvailable;
    }
    
    /** Check async for updates and inform user.
     *  <br/> Fails on missing network connection?!
     *  @param reloadServer True to get mapDate from server, false to compare this map with local mapTime. **/
    public void checkUpdateAvailableMsg(final Activity activity)
    {
      log("GH Checking for update available ...");
      if (getLastCheckTimeSpan()<(1000*60*60))
      {
        log("GH Do not check again until one hour.");
        return;
      }
      final String msgTxt = "Update for map is available: " + getMapName() + "\nPlease update map!";
      if (updateAvailable)
      {
        log("GH Do not check again, update is available!");
        logUserLong(msgTxt, activity);
        return;
      }
      lastCheckTime = System.currentTimeMillis();
      new AsyncTask<Void, Void, MyMap>() {

        @Override protected void onPostExecute(MyMap myMapRemote) {
            super.onPostExecute(myMapRemote);
            if (myMapRemote==null) { return; } // No map on server.
            if (myMapRemote.isUpdateAvailable())
            {
              MyMap.this.updateAvailable = true;
              logUserLong(msgTxt, activity);
            }
        }

        @Override
        protected MyMap doInBackground(Void... params)
        {
          OnProgressListener l = new OnProgressListener()
          {
            @Override
            public void onProgress(int progress) {}
          };
          List<MyMap> myMaps = DownloadMapActivity.getMapsFromJSsources(MyMap.this.getMapName(),l);
          for (MyMap curMap : myMaps)
          {
            if (curMap.equals(MyMap.this))
            {
              return curMap;
            }
          }
          return null;
        }
      }.execute();
    }
    
    private static String readTimeLocal(String mapName)
    {
      File versFile = getVersionFile(mapName);
      String mapVers = null;
      if (versFile.exists())
      {
        mapVers = IO.readFromFile(versFile, "\n");
      }
      String timeLocal = "1970-01";
      if (mapVers != null)
      {
        String fields[] = mapVers.split("\n");
        if (fields.length>1) { timeLocal = fields[1]; }
      }
      return timeLocal;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

    public String getSize() {
        if (size == "") {
            return "Map size: < 1M";
        }
        return "Map size: " + size;
    }

    public void setSize(String size) {
        this.size = size;
    }

    /** The url, where to download the map. On local maps we have only the maps folder instead. **/
    public String getUrl() {
        return url;
    }

    /** The url, where to download the map. On local maps we have only the maps folder instead. **/
    public void setUrl(String url) {
        this.url = url;
    }
    
  public String getTime(boolean ofRemote)
  {
    if (ofRemote)
    {
      return timeRemote;
    }
    if (timeLocal.isEmpty()) { timeLocal = readTimeLocal(getMapName()); }
    return timeLocal;
  }

  public void setTime(boolean ofRemote, String time)
  {
    if (ofRemote)
    {
      this.timeRemote = time;
    }
    this.timeLocal = time;
  }
  
    public String getContinent() {
        return continent;
    }

    private void setContinent(String continent) {
        this.continent = continent;
    }

    public String getMapName() {
        return mapName;
    }

    public void setMapName(String mapName) {
        this.mapName = mapName;
    }

    /** When mapName is deprecated map **/
    public String getMapNameNew() {
        return mapNameNew;
    }

    /** When mapName is deprecated map **/
    public void setMapNameNew(String mapNameNew) {
        this.mapNameNew = mapNameNew;
    }

    public DlStatus getStatus() {
        return status;
    }

    public void setStatus(DlStatus status) {
        this.status = status;
    }

    @Override
    public int compareTo(MyMap o) {
        if (getStatus() != DlStatus.On_server && o.getStatus() == DlStatus.On_server) {
            return -1;
        }
        if (getStatus() == DlStatus.On_server && o.getStatus() != DlStatus.On_server) {
            return 1;
        }
        return ((getContinent() + getCountry()).compareToIgnoreCase(o.getContinent() + o.getCountry()));
    }

    @Override
    public boolean equals(Object other)
    {
      if (other instanceof MyMap)
      {
        MyMap otherM = (MyMap) other;
        return otherM.getMapName().equals(getMapName());
      }
      return false;
    }
    
    public String toString() {
        return "MyMap{" +
                "country='" + country + '\'' +
                ", size='" + size + '\'' +
                ", url='" + url + '\'' +
                ", continent='" + continent + '\'' +
                ", mapName='" + mapName + '\'' +
                ", mapNameNew='" + mapNameNew + '\'' +
                ", resId=" + resId +
                ", timeRemote=" + timeRemote +
                ", timeLocal=" + timeLocal +
                ", status=" + getStatus() +
                '}';
    }

    private static void log(String str) {
        Log.i(MyMap.class.getName(), str);
    }
    
    private static void logUserLong(String str, Activity activity) {
      Log.i(MyMap.class.getName(), str);
      Toast.makeText(activity.getBaseContext(), str, Toast.LENGTH_LONG).show();
    }
} public enum DlStatus {Downloading, Unzipping, Complete, On_server, Error}; public interface OnClickMapListener {
    /**
     * Tell Activity what to do when map FAB is clicked
     *
     * @param pos The map-position on ArrayAdapter.
     */
    void onClickMap(int pos);
} public interface OnProgressListener {
    void onProgress(int progress);
} public class IO
{
  public static boolean writeToFile(String txt, File file, boolean append)
  {
    try(FileWriter sw = new FileWriter(file, append);
        BufferedWriter bw = new BufferedWriter(sw))
    {
      bw.write(txt);
      bw.flush();
    }
    catch (IOException e)
    {
      e.printStackTrace();
      return false;
    }
    return true;
  }
  
  public static String readFromFile(File file, String lineBreak)
  {
    StringBuilder sb = new StringBuilder();
    try(FileReader sr = new FileReader(file);
        BufferedReader br = new BufferedReader(sr))
    {
      while (true)
      {
        String txt = br.readLine();
        if (txt == null) { break; }
        sb.append(txt).append(lineBreak);
      }
    }
    catch (IOException e)
    {
      e.printStackTrace();
      return null;
    }
    return sb.toString();
  }
  
  /**
   * Return the size of a directory in megabytes
   */
  public static long dirSize(File dir)
  {
    if (!dir.exists()) return 0;

    long result = 0;
    for (File file : dir.listFiles())
    { // Recursive call if it's a directory
      if (file.isDirectory())
      {
        result += dirSize(file);
      }
      else
      {
        result += file.length();
      }
    }
    return result / (1024 * 1024);
  }
  
  /** From Android 8 (Orio) there is a bug for downloading to sd-card.
   * <br/>In this case use an internal storage for download.
   * @param requestedDir The external directory that is desired. */
  public static File getDownloadDirectory(File requestedDir, Context context)
  {
    if (Build.VERSION.SDK_INT >= 26) // OREO
    { // We just assume Download-Dir is mounted.
      return context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
    }
    return requestedDir;
  }

  public static File getDefaultBaseDirectory(Context context)
  {
    if (Build.VERSION.SDK_INT >= 29)
    { // ExternalStoragePublicDirectory Deprecated since android Q
      File target = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
      if (!Environment.getExternalStorageState(target).equals(Environment.MEDIA_MOUNTED))
      {
        Toast.makeText(context, "Pocket Maps is not usable without an external storage!", Toast.LENGTH_SHORT).show();
        return null;
      }
      return target;
    }
    else if (Build.VERSION.SDK_INT >= 19)
    { // greater or equal to Kitkat
      if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
      {
        Toast.makeText(context, "Pocket Maps is not usable without an external storage!", Toast.LENGTH_SHORT).show();
        return null;
      }
      return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
    }
    else
    {
      return Environment.getExternalStorageDirectory();
    }
  }
  
  public static ArrayList<String> listSelectionPaths(Context context, boolean cacheDir)
  {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
    {
      return listSelectionPathsLollipop(context, cacheDir);
    }
    else
    {
      ArrayList<String> list = new ArrayList<String>();
      list.add(getDefaultBaseDirectory(context).getPath());
      return list;
    }
  }
  
  public static void showRootfolderSelector(Activity activity, boolean cacheDir, Runnable callback)
  {
    AlertDialog.Builder builder1 = new AlertDialog.Builder(activity);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
    {
      builder1.setTitle(R.string.switch_maps_dir);
      builder1.setCancelable(true);
      addSelection(builder1, cacheDir, callback, activity);
    }
    else
    {
      builder1.setMessage(R.string.needs_vers_lollipop);
      builder1.setCancelable(false);
    }
    AlertDialog alert11 = builder1.create();
    alert11.show();
  }

  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  private static void addSelection(AlertDialog.Builder builder1, boolean cacheDir, final Runnable callback, Activity activity)
  {
    String curFolder = Variable.getVariable().getMapsFolder().getPath();
    int curSelected = 0;
    final ArrayList<String> items = listSelectionPathsLollipop(activity, cacheDir);
    final ArrayList<String> itemsText = new ArrayList<String>();
    for (String item : items)
    {
      itemsText.add(getStorageText(new File(item)));
      if (curFolder.startsWith(item))
      {
        curSelected = items.indexOf(item);
      }
    }
    OnClickListener listener = new OnClickListener()
    {
      @Override
      public void onClick(DialogInterface dialog, int buttonNr)
      {
        String selection = items.get(buttonNr);
        Variable.getVariable().setBaseFolder(selection);
        File mapsFolder = Variable.getVariable().getMapsFolder();
        if (!mapsFolder.exists()) { mapsFolder.mkdirs(); }
        Variable.getVariable().saveVariables(Variable.VarType.Base);
        Variable.getVariable().getLocalMaps().clear();
        dialog.dismiss();
        callback.run();
      }
    };
    builder1.setSingleChoiceItems(itemsText.toArray(new String[0]), curSelected, listener);
  }

  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  private static ArrayList<String> listSelectionPathsLollipop(Context context, boolean cacheDir)
  {
    File files[];
    if (cacheDir)
    {
      files = context.getExternalCacheDirs();
    }
    else
    {
      files = context.getExternalMediaDirs();
    }
    final ArrayList<String> items = new ArrayList<String>();
    items.add(getDefaultBaseDirectory(context).getPath());
    for (File curFile : files)
    {
      if (curFile == null)
      { // Regarding to android javadoc this may be possible.
        continue;
      }
      String mountState = Environment.getExternalStorageState(curFile);
      if (mountState.equals(Environment.MEDIA_MOUNTED) ||
          mountState.equals(Environment.MEDIA_SHARED))
      {
        items.add(curFile.getPath());
      }
    }
    return items;
  }
  
  private static String getStorageText(File dir)
  {
    long freeSpace = dir.getFreeSpace() / (1024*1024);
    long totalSpace = dir.getTotalSpace() / (1024*1024);
    return "[ " + freeSpace + " MB free / " + totalSpace + " MB total]\n" + dir.getPath() + "\n";
  }
} public class SetStatusBarColor {

    public SetStatusBarColor() {
    }

    /**
     * set (statusBar: systemBar + actionBar) View to (color) with given (activity)
     *
     * @param statusBar View
     * @param color     int
     * @param activity  FragmentActivity
     */
    public void setStatusBarColor(View statusBar, int color, Activity activity) {
        //        System.out.println("------------------" + statusBar + "--" + color + "--" + activity);
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                Window w = activity.getWindow();
                w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
                        WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
                //status bar height
                int actionBarHeight = getActionBarHeight(activity);
                int statusBarHeight = getStatusBarHeight(activity);
                //action bar height
                statusBar.getLayoutParams().height = actionBarHeight + statusBarHeight;
                statusBar.setBackgroundColor(color);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * set (systemBar only) View to (color) with given (activity)
     *
     * @param statusBar View
     * @param color     int
     * @param activity  FragmentActivity
     */
    public void setSystemBarColor(View statusBar, int color, Activity activity) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                Window w = activity.getWindow();
                w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
                        WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
                statusBar.getLayoutParams().height = getStatusBarHeight(activity);
                statusBar.setBackgroundColor(color);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @param activity
     * @return action bar height
     */
    public int getActionBarHeight(Activity activity) {
        int actionBarHeight = 0;
        TypedValue tv = new TypedValue();
        if (activity.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
            actionBarHeight =
                    TypedValue.complexToDimensionPixelSize(tv.data, activity.getResources().getDisplayMetrics());
        }
        return actionBarHeight;
    }

    /**
     * @param activity
     * @return status bar height
     */
    public int getStatusBarHeight(Activity activity) {
        int result = 0;
        int resourceId = activity.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = activity.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }
} public class Variable {
    public enum TravelMode{Foot, Bike, Car};
    public enum VarType{Base, Geocode};
    private TravelMode travelMode;
    private HashMap<VarType, Boolean> loadStatus = new HashMap<VarType, Boolean>();
    /**
     * fastest, shortest (route)
     */
    private String weighting;
    /**
     * Bidirectional Dijkstra:      DIJKSTRA_BI             = "dijkstrabi"
     * <p/>
     * Unidirectional Dijkstra:     DIJKSTRA                = "dijkstra"
     * <p/>
     * one to many Dijkstra:        DIJKSTRA_ONE_TO_MANY    = "dijkstraOneToMany"
     * <p/>
     * Unidirectional A* :          ASTAR                   = "astar"
     * <p/>
     * Bidirectional A* :           ASTAR_BI                = "astarbi"
     */
    private String routingAlgorithms;
    /**
     * instructions on or off; default true (on)
     */
    private boolean directionsON;
    /**
     * Show a sign while navigating, that indicates max speed
     */
    private boolean showSpeedLimits;
    /**
     * Warn the user if current speed is more than max allowed speed
     */
    private boolean speakSpeedLimits;
    /**
     * users current / last used zoom level
     */
    private int lastZoomLevel;

    /**
     * users last browsed screen center location
     */
    private GeoPoint lastLocation;
    /**
     * map directory name: pocketmaps/maps/
     */
    private String mapDirectory;
    /**
     * download directory name: pocketmaps/downloads/
     */
    private String dlDirectory;
    /**
     * map directory name: pocketmaps/tracking/
     */
    private String trackingDirectory;

    /**
     * The default map to startup.
     */
    private boolean autoSelectMap;
    
    /**
     * The selected speech engine, null for AndroidTTS.
     */
    private String ttsEngine;
    private String ttsWantedVoice;

    /**
     * area or country name (need to be loaded)
     * <p/>
     * example: /storage/emulated/0/Download/(mapDirectory)/(country)-gh
     */
    private String country;
    /**
     * a File where all Areas or counties are in
     * <p/>
     * example:
     * <p/>
     * <li>mapsFolder.getAbsolutePath() = /storage/emulated/0/Download/pocketmaps/maps </li>
     * <p/>
     * <li> mapsFolder   =   new File("/storage/emulated/0/Download/pocketmaps/maps")</li>
     */
    private File mapsFolder;

    /**
     * Server for download JSON list of maps
     */
    private String mapUrlJSON;
    /**
     * prepare to load the map
     */
    private volatile boolean prepareInProgress;
    
    private boolean bImperalUnit = false;

    /**
     * list of downloaded maps in local storage; check and init when app started; used to avoid recheck local files
     */
    private List<MyMap> localMaps;
    /**
     * temporary memorialize recent downloaded maps from DownloadMapActivity
     */
    private List<MyMap> recentDownloadedMaps;

    /**
     * temporary memorialize download list of cloud maps from DownloadMapActivity
     */
    private List<MyMap> cloudMaps;
    
    /**
     * sport category spinner index at {@link Analytics#spinner}
     */
    private int sportCategoryIndex;

    private boolean lightSensorON;
    private boolean voiceON;
    private boolean smoothON;
    
    private int offlineSearchBits = GeocoderLocal.BIT_CITY + GeocoderLocal.BIT_STREET;
    private int geocodeSearchEngine = 0;
    private String geocodeSearchTextList = "";
    
    public static final int DEF_ZOOM = 8;
    public static final String DEF_WIGHTING = "fastest";
    public static final String DEF_ALG = "astarbi";

    /**
     * application context
     */
    private Context context;

    private static Variable variable;

    private Variable() {
        this.travelMode = TravelMode.Foot;
        this.weighting = DEF_WIGHTING;
        this.routingAlgorithms = DEF_ALG;
        this.autoSelectMap = false;
        this.ttsEngine = null;
        this.ttsWantedVoice = null;
        this.lastZoomLevel = DEF_ZOOM;
        this.lastLocation = null;
        this.country = null;
        this.mapsFolder = null;
        this.context = null;
        this.directionsON = true;
        this.showSpeedLimits = false;
        this.speakSpeedLimits = false;
        this.voiceON = true;
        this.lightSensorON = true;
        this.smoothON = false;
        this.mapDirectory = "pocketmaps/maps/";
        this.dlDirectory = "pocketmaps/downloads/";
        this.trackingDirectory = "pocketmaps/tracking/";
        this.mapUrlJSON = "http://vsrv15044.customer.xenway.de/maps";
        this.localMaps = new ArrayList<>();
        this.recentDownloadedMaps = new ArrayList<>();
        this.cloudMaps = new ArrayList<>();
        this.sportCategoryIndex = 0;
    }

    public static Variable getVariable() {
        if (variable == null) {
            variable = new Variable();
        }
        return variable;
    }
    
    public boolean isLoaded(VarType type)
    {
      if (loadStatus.containsKey(type)) { return true; }
      return false;
    }

    public String getMapUrlJSON() {
        return mapUrlJSON;
    }

    public TravelMode getTravelMode() {
        return travelMode;
    }

    public void setTravelMode(TravelMode travelMode) {
        this.travelMode = travelMode;
    }
    
    public boolean getAutoSelectMap()
    {
        return autoSelectMap;
    }
    
    public void setAutoSelectMap(boolean autoSelectMap)
    {
        this.autoSelectMap = autoSelectMap;
    }
    
    public String getTtsEngine()
    {
        return ttsEngine;
    }
    
    public void setTtsEngine(String ttsEngine)
    {
        this.ttsEngine = ttsEngine;
    }
    
    public String getTtsWantedVoice()
    {
        return ttsWantedVoice;
    }
    
    public void setTtsWantedVoice(String ttsWantedVoice)
    {
        this.ttsWantedVoice = ttsWantedVoice;
    }
    
    public int getGeocodeSearchEngine()
    {
      return geocodeSearchEngine;
    }
    
    public boolean setGeocodeSearchEngine(int geocodeSearchEngine)
    {
      if (this.geocodeSearchEngine == geocodeSearchEngine) { return false; }
      this.geocodeSearchEngine = geocodeSearchEngine;
      return true;
    }
    
    public int getOfflineSearchBits()
    {
      return offlineSearchBits;
    }
    
    public void setOfflineSearchBits(int offlineSearchBits)
    {
      this.offlineSearchBits = offlineSearchBits;
    }
    
    public String[] getGeocodeSearchTextList()
    {
      if (geocodeSearchTextList.isEmpty()) { return new String[0]; }
      return geocodeSearchTextList.trim().split("\n");
    }
    
    public boolean addGeocodeSearchText(String text)
    {
      if (text.isEmpty()) { return false; }
      text = text.toLowerCase();
      for (String curTxt : getGeocodeSearchTextList())
      {
        if (curTxt.contains(text)) { return false; }
      }
      geocodeSearchTextList = geocodeSearchTextList + "\n" + text;
      return true;
    }

    public String getWeighting() {
        return weighting;
    }

    public void setWeighting(String weighting) {
        this.weighting = weighting;
    }

    public String getRoutingAlgorithms() {
        return routingAlgorithms;
    }

    public void setRoutingAlgorithms(String routingAlgorithms) {
        this.routingAlgorithms = routingAlgorithms;
    }

    public boolean isDirectionsON() {
        return directionsON;
    }

    public void setDirectionsON(boolean directionsON) {
        this.directionsON = directionsON;
    }

    public boolean isShowingSpeedLimits() {
        return showSpeedLimits;
    }

    public void setShowSpeedLimits(boolean showSpeedLimits) {
        this.showSpeedLimits = showSpeedLimits;
    }

    public boolean isSpeakingSpeedLimits() {
        return speakSpeedLimits;
    }

    public void setSpeakSpeedLimits(boolean speakSpeedLimits) {
        this.speakSpeedLimits = speakSpeedLimits;
    }
    
    public boolean isVoiceON()
    {
      return voiceON;
    }
    
    public void setVoiceON(boolean voiceON)
    {
      this.voiceON = voiceON;
    }
    
    public boolean isLightSensorON()
    {
      return lightSensorON;
    }
    
    public void setLightSensorON(boolean lightSensorON)
    {
      this.lightSensorON = lightSensorON;
    }

    public boolean isSmoothON()
    {
        return smoothON;
    }

    public void setSmoothON(boolean smoothON)
    {
        this.smoothON = smoothON;
    }

    /**
     * @return is DirectionsON as string : "true or false"
     */
    public String getDirectionsON() {
        return isDirectionsON() ? "true" : "false";
    }

    public int getLastZoomLevel() {
        return lastZoomLevel;
    }

    public void setLastZoomLevel(int lastZoomLevel) {
        this.lastZoomLevel = lastZoomLevel;
    }
    
    public boolean isImperalUnit() {
      return bImperalUnit;
    }
    
    public void setImperalUnit(boolean bImperalUnit) {
        this.bImperalUnit = bImperalUnit;
    }

    public GeoPoint getLastLocation() {
        return lastLocation;
    }

    public void setLastLocation(GeoPoint lastLocation) {
        this.lastLocation = lastLocation;
    }

    /** Returns the last selected country, or an empty String. **/
    public String getCountry() {
        if (country==null) { return ""; }
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

    public File getMapsFolder() {
        return mapsFolder;
    }
    
    public File getDownloadsFolder() {
      File dlFolder = new File(mapsFolder.getParentFile().getParent(), dlDirectory);
      if (!dlFolder.exists()) { dlFolder.mkdirs(); }
      return IO.getDownloadDirectory(dlFolder, context);
    }

    public void setBaseFolder(String baseFolder) {
        this.mapsFolder = new File(baseFolder, mapDirectory);
    }

    public File getTrackingFolder() {
      return new File(mapsFolder.getParentFile().getParent(), trackingDirectory);
    }

    public Context getContext() {
        return context;
    }

    public void setContext(Context context) {
        this.context = context;
    }

    public boolean isPrepareInProgress() {
        return prepareInProgress;
    }

    public void setPrepareInProgress(boolean prepareInProgress) {
        this.prepareInProgress = prepareInProgress;
    }

    public List<MyMap> getLocalMaps() {
        return localMaps;
    }

    /**
     * add a list of maps to localMaps
     *
     * @param localMaps list of maps
     */
    public void addLocalMaps(List<MyMap> localMaps) {
        this.localMaps.addAll(localMaps);
    }

    /**
     * add a map to local map list
     *
     * @param localMap MyMap
     */
    public void addLocalMap(MyMap localMap) {
        if (!getLocalMapNameList().contains(localMap.getMapName())) {
            this.localMaps.add(localMap);
        }
    }

    public void removeLocalMap(MyMap localMap) {
        this.localMaps.remove(localMap);
    }

    public void setLocalMaps(List<MyMap> localMaps) {
        this.localMaps = localMaps;
    }

    /**
     * @return a string list of local map names (continent_country)
     */
    public List<String> getLocalMapNameList() {
        ArrayList<String> al = new ArrayList<String>();
        for (MyMap mm : getLocalMaps()) {
            al.add(mm.getMapName());
        }
        return al;
    }

    public List<MyMap> getRecentDownloadedMaps() {
        return recentDownloadedMaps;
    }

    public List<MyMap> getCloudMaps() {
        return cloudMaps;
    }

    public void updateCloudMaps(List<MyMap> cloudMaps) {
      ArrayList<MyMap> newList = new ArrayList<MyMap>();
      for (MyMap oldMap : this.cloudMaps)
      {
        for (MyMap newMap : cloudMaps)
        {
          if (newMap.getUrl().equals(oldMap.getUrl()))
          {
            newMap.setStatus(oldMap.getStatus());
            break;
          }
        }
      }
      // Find same Map from CloudMaps
      for (MyMap newMap : cloudMaps)
      {
        int myIndex = Variable.getVariable().getCloudMaps().indexOf(newMap);
        if (myIndex < 0) { newList.add(newMap); continue; }
        MyMap sameMap = Variable.getVariable().getCloudMaps().get(myIndex);
        sameMap.set(newMap);
        newList.add(sameMap);
      }
      this.cloudMaps = newList;
    }

    /** Similar as getTravelMode() but used for spinner. **/
    public int getSportCategoryIndex() {
        return sportCategoryIndex;
    }

    /** Similar as setTravelMode() but used for spinner. **/
    public void setSportCategoryIndex(int sportCategoryIndex) {
        this.sportCategoryIndex = sportCategoryIndex;
    }

    /**
     * run when app open at run time
     * <p/>
     * load variables from saved file
     *
     * @return true if load succeed, false if nothing to load or load fail
     */
    public boolean loadVariables(VarType varType)
    {
      String content;
      if (varType == VarType.Base)
      {
        content = readFile("pocketmapssavedfile.txt");
        loadStatus.put(VarType.Base, Boolean.TRUE);
      }
      else
      {
        content = readFile("pocketmapssavedfile_" + varType + ".txt");
        loadStatus.put(VarType.Geocode, Boolean.TRUE);
      }
      if (content == null)
      {
        return false;
      }
      JsonWrapper jo;
      try
      {
        jo = new JsonWrapper(content);
        if (varType == VarType.Base)
        {
            setTravelMode(TravelMode.valueOf(toUpperFirst(jo.getStr("travelMode", TravelMode.Foot.toString()))));
            setWeighting(jo.getStr("weighting", DEF_WIGHTING));
            setRoutingAlgorithms(jo.getStr("routingAlgorithms", DEF_ALG));
            setDirectionsON(jo.getBool("directionsON", true));
            setSpeakSpeedLimits(jo.getBool("speakSpeedLimits", false));
            setShowSpeedLimits(jo.getBool("showSpeedLimits", false));
            setVoiceON(jo.getBool("voiceON", true));
            setLightSensorON(jo.getBool("lightSensorON", true));
            setAutoSelectMap(jo.getBool("autoSelectMap", false));
            setTtsEngine(jo.getStr("ttsEngine", null));
            setTtsWantedVoice(jo.getStr("ttsWantedVoice", null));
            setLastZoomLevel(jo.getInt("lastZoomLevel", DEF_ZOOM));
            setImperalUnit(jo.getBool("isImperalUnit", false));
            setSmoothON(jo.getBool("smoothON", false));
            double la = jo.getDouble("latitude", 0);
            double lo = jo.getDouble("longitude", 0);
            if (la != 0 && lo != 0) {
                setLastLocation(new GeoPoint(la, lo));
            }
            String coun = jo.getStr("country", "");
            if (!coun.isEmpty()) {
                setCountry(coun);
            }
            String mapsFolderAbsStr = jo.getStr("mapsFolderAbsPath", "/x/y/z");
            File mapsFolderAbsPath = new File(mapsFolderAbsStr);
            if (mapsFolderAbsPath.exists() && !mapsFolderAbsStr.equals("/x/y/z"))
            {
              setBaseFolder(mapsFolderAbsPath.getParentFile().getParent());
            }
            setSportCategoryIndex(jo.getInt("sportCategoryIndex", 0));
        }
        else if (varType == VarType.Geocode)
        {
          setGeocodeSearchEngine(jo.getInt("geocodeSearchEngine", 0));
          geocodeSearchTextList = jo.getStr("geocodeSearchTextList", "");
          offlineSearchBits = jo.getInt("offlineSearchBits", GeocoderLocal.BIT_CITY + GeocoderLocal.BIT_STREET);
        }
      }
      catch (JSONException e)
      {
          e.printStackTrace();
          return false;
      }
      return true;
    }

    private static String toUpperFirst(String string)
    {
      // TODO This is just a workaround, because of incompatiblity from older versions.
      // This workaround will ensure to override the fault data.
      // Can be deleted later, maybe in year 2022
      if (string == null) { return "Car"; }
      String first = string.substring(0,1).toUpperCase();
      String rest = string.substring(1);
      return first + rest;
    }

    /**
     * run before app destroyed at run time
     * <p/>
     * save variables to local file (json)   @return true is succeed, false otherwise
     */
    public boolean saveVariables(VarType varType) {
        JsonWrapper jo = new JsonWrapper();
        try
        {
          if (varType == VarType.Base)
          {
            jo.put("travelMode", getTravelMode().toString());
            jo.put("weighting", getWeighting());
            jo.put("routingAlgorithms", getRoutingAlgorithms());
            jo.put("directionsON", isDirectionsON());
            jo.put("showSpeedLimits", isShowingSpeedLimits());
            jo.put("speakSpeedLimits", isSpeakingSpeedLimits());
            jo.put("voiceON", isVoiceON());
            jo.put("lightSensorON", isLightSensorON());
            jo.put("autoSelectMap", autoSelectMap);
            jo.put("ttsEngine", ttsEngine);
            jo.put("ttsWantedVoice", ttsWantedVoice);
            jo.put("lastZoomLevel", getLastZoomLevel());
            if (getLastLocation() != null) {
                jo.put("latitude", getLastLocation().getLatitude());
                jo.put("longitude", getLastLocation().getLongitude());
            } else {
                jo.put("latitude", 0);
                jo.put("longitude", 0);
            }
            jo.put("isImperalUnit", bImperalUnit);
            jo.put("country", getCountry());
            jo.put("smoothON", smoothON);
            jo.put("mapsFolderAbsPath", getMapsFolder().getAbsolutePath());
            jo.put("sportCategoryIndex", getSportCategoryIndex());
          }
          else if (varType == VarType.Geocode)
          {
            jo.put("geocodeSearchEngine", geocodeSearchEngine);
            jo.put("geocodeSearchTextList", geocodeSearchTextList);
            jo.put("offlineSearchBits", offlineSearchBits);
          }
        } catch (JSONException e) {
            e.printStackTrace();
            return false;
        }
        if (varType == VarType.Base)
        {
          return saveStringToFile("pocketmapssavedfile.txt", jo.toString());
        }
        else
        {
          return saveStringToFile("pocketmapssavedfile_" + varType + ".txt", jo.toString());
        }
    }
    
    /** Return existing files, where settings are saved. **/
    public ArrayList<String> getSavingFiles()
    {
      ArrayList<String> list = new ArrayList<String>();
      for (String f : context.fileList())
      {
        if (isSavingFile(f)) { list.add(f); }
      }
      return list;
    }
    
    public static boolean isSavingFile(String f)
    {
      return f.startsWith("pocketmapssavedfile") && f.endsWith(".txt");
    }

    /**
     * @return read saved file and return it as a string
     */
    private String readFile(String file) {
        try(FileInputStream fis = context.openFileInput(file);
            InputStreamReader isr = new InputStreamReader(fis);
            BufferedReader bufferedReader = new BufferedReader(isr))
        {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                sb.append(line);
            }
            return sb.toString();
        } catch (IOException e) {
            log("Cant load savingfile, maybe the first time since app installed.");
            e.printStackTrace();
            return null;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * @param file a string need to be saved
     * @return
     */
    private boolean saveStringToFile(String file, String content) {
        try(FileOutputStream fos = context.openFileOutput(file, Context.MODE_PRIVATE))
        {
            fos.write(content.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
    
    private void log(String s) {
      Log.i(Variable.class.getName(), s);
    }

}
import android.app.Activity;
import android.app.DownloadManager;
import android.app.DownloadManager.Request;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.SearchView.OnCloseListener;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.junjunguo.pocketmaps.R;
import com.junjunguo.pocketmaps.downloader.DownloadFiles;
import com.junjunguo.pocketmaps.downloader.MapDownloadUnzip;
import com.junjunguo.pocketmaps.downloader.MapDownloadUnzip.StatusUpdate;
import com.junjunguo.pocketmaps.fragments.MyMapAdapter;
import com.junjunguo.pocketmaps.model.MyMap;
import com.junjunguo.pocketmaps.model.MyMap.DlStatus;
import com.junjunguo.pocketmaps.model.listeners.OnClickMapListener;
import com.junjunguo.pocketmaps.model.listeners.OnProgressListener;
import com.junjunguo.pocketmaps.util.IO;
import com.junjunguo.pocketmaps.util.SetStatusBarColor;
import com.junjunguo.pocketmaps.util.Variable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
            Variable.getVariable().updateCloudMaps(myMaps);
            cloudMapsTime = System.currentTimeMillis();
            myDownloadAdapter.clearList();
            myDownloadAdapter.addAll(filterDeprecatedMaps(myMaps));
        }
    }

    private List<MyMap> filterDeprecatedMaps(List<MyMap> myMaps)
    {
      ArrayList<MyMap> newList = new ArrayList<MyMap>();
      for (MyMap curMap : myMaps)
      {
        if (!curMap.getMapNameNew().isEmpty() &&
             curMap.getStatus() != DlStatus.Complete) { continue; }
        newList.add(curMap);
      }      
      return newList;
    }

    /**
     * active directions, and directions view
     */
    private void activateRecyclerView(List<MyMap> myMaps) {
        mapsRV = (RecyclerView) findViewById(R.id.my_maps_download_recycler_view);
        DefaultItemAnimator animator = new DefaultItemAnimator();
        animator.setAddDuration(600);
        animator.setRemoveDuration(600);
        mapsRV.setItemAnimator(animator);
        // use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        mapsRV.setHasFixedSize(true);

        // use a linear layout manager
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        mapsRV.setLayoutManager(layoutManager);
        myDownloadAdapter = new MyMapAdapter(myMaps, this, true);
        mapsRV.setAdapter(myDownloadAdapter);
    }

    @Override public void onClickMap(int iPos) {
        try {
            // download map
          onClickMapNow(iPos);
        } catch (Exception e) {e.printStackTrace();}
    }

    /**
     * download map
     *
     * @param view     View
     * @param position item position
     */
    private void onClickMapNow(int position)
    {
      MyMap myMap = myDownloadAdapter.getItem(position);
      if (myMap.getStatus() == MyMap.DlStatus.Downloading || myMap.getStatus() == MyMap.DlStatus.Unzipping)
      {
        logUser("Already downloading!");
        return;
      }
      else if (myMap.isUpdateAvailable())
      {
        MyMap myMapNew = null;
        if (!myMap.getMapNameNew().isEmpty())
        {          
          int removePos = -1;
          for (int i= 0; i<myDownloadAdapter.getItemCount(); i++)
          {
            if (myDownloadAdapter.getItem(i).getMapName().equals(myMap.getMapName()))
            {
              removePos = i;
            }
            if (myDownloadAdapter.getItem(i).getMapName().equals(myMap.getMapNameNew()))
            {
              position = i;
              myMapNew = myDownloadAdapter.getItem(i);
            }
          }
          if (removePos < 0 || myMapNew == null)
          {
            logUser("OldMap or NewMap missing on json-list!");
            return;
          }
          mapsRV.scrollToPosition(position);
          myDownloadAdapter.remove(removePos);
          if (position > removePos) { position--; }
        }
        MainActivity.clearLocalMap(myMap);
        if (myMapNew != null)
        {
          myMap = myMapNew;
          if (myMap.getStatus() != DlStatus.On_server)
          {
            logUser("New map is: " + myMap.getMapName());
            return;
          }
        }
      }
      else if (myMap.getStatus() == MyMap.DlStatus.Complete)
      {
        logUser("Already downloaded!");
        return;
      }
      myMap.setStatus(MyMap.DlStatus.Downloading);
      myDownloadAdapter.refreshMapView(myMap);
      String vers = "?v=unknown";
      DownloadManager dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
      try
      {
        PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
        vers = "?v=" + packageInfo.versionName;
      }
      catch (Exception e) {} // No problem, not important.
      Request request = new Request(Uri.parse(myMap.getUrl() + vers));
      File destFile = MyMap.getMapFile(myMap, MyMap.MapFileType.DlMapFile);
      request.setDestinationUri(Uri.fromFile(destFile));
      request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
      request.setMimeType("application/pocketmaps");
      long enqueueId = dm.enqueue(request);
      File idFile = MyMap.getMapFile(myMap, MyMap.MapFileType.DlIdFile);
      IO.writeToFile("" + enqueueId, idFile, false);