diff --git a/android/src/com/mapswithme/maps/DownloadResourcesActivity.java b/android/src/com/mapswithme/maps/DownloadResourcesActivity.java index a1391a7e5a..cc2c467c75 100644 --- a/android/src/com/mapswithme/maps/DownloadResourcesActivity.java +++ b/android/src/com/mapswithme/maps/DownloadResourcesActivity.java @@ -1,13 +1,5 @@ package com.mapswithme.maps; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import android.annotation.SuppressLint; import android.content.ActivityNotFoundException; import android.content.ContentResolver; @@ -32,9 +24,18 @@ import com.mapswithme.maps.api.ParsedMmwRequest; import com.mapswithme.maps.base.MapsWithMeBaseActivity; import com.mapswithme.maps.location.LocationService; import com.mapswithme.util.ConnectionState; +import com.mapswithme.util.Constants; import com.mapswithme.util.Utils; import com.mapswithme.util.statistics.Statistics; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + @SuppressLint("StringFormatMatches") public class DownloadResourcesActivity extends MapsWithMeBaseActivity implements LocationService.Listener, MapStorage.Listener @@ -432,7 +433,7 @@ public class DownloadResourcesActivity extends MapsWithMeBaseActivity String path = null; File tmpFile = null; final String scheme = data.getScheme(); - if (scheme != null && !scheme.equalsIgnoreCase("file")) + if (scheme != null && !scheme.equalsIgnoreCase(Constants.Url.DATA_SCHEME_FILE)) { // scheme is "content" or "http" - need to download file first InputStream input = null; diff --git a/android/src/com/mapswithme/maps/MWMActivity.java b/android/src/com/mapswithme/maps/MWMActivity.java index c0c5ff02d5..ae9e139412 100644 --- a/android/src/com/mapswithme/maps/MWMActivity.java +++ b/android/src/com/mapswithme/maps/MWMActivity.java @@ -40,6 +40,8 @@ import com.mapswithme.maps.location.LocationService; import com.mapswithme.maps.promo.ActivationSettings; import com.mapswithme.maps.search.SearchController; import com.mapswithme.maps.settings.SettingsActivity; +import com.mapswithme.maps.settings.StoragePathManager; +import com.mapswithme.maps.settings.StoragePathManager.SetStoragePathListener; import com.mapswithme.maps.settings.UnitLocale; import com.mapswithme.maps.widget.MapInfoView; import com.mapswithme.maps.widget.MapInfoView.OnVisibilityChangedListener; @@ -47,8 +49,6 @@ import com.mapswithme.maps.widget.MapInfoView.State; import com.mapswithme.util.ConnectionState; import com.mapswithme.util.Constants; import com.mapswithme.util.ShareAction; -import com.mapswithme.util.StoragePathManager; -import com.mapswithme.util.StoragePathManager.SetStoragePathListener; import com.mapswithme.util.UiUtils; import com.mapswithme.util.Utils; import com.mapswithme.util.Yota; @@ -365,7 +365,7 @@ public class MWMActivity extends NvEventQueueActivity if (!kmlMoved) { - if (mPathManager.MoveBookmarks()) + if (mPathManager.moveBookmarks()) mApplication.nativeSetBoolean(KmlMovedFlag, true); else { @@ -379,19 +379,19 @@ public class MWMActivity extends NvEventQueueActivity SetStoragePathListener listener = new SetStoragePathListener() { @Override - public void MoveFilesFinished(String newPath) + public void moveFilesFinished(String newPath) { mApplication.nativeSetBoolean(KitKatMigrationCompleted, true); ShowAlertDlg(R.string.kitkat_migrate_ok); } @Override - public void MoveFilesFailed() + public void moveFilesFailed() { ShowAlertDlg(R.string.kitkat_migrate_failed); } }; - mPathManager.CheckWritableDir(this, listener); + mPathManager.checkWritableDir(this, listener); } } @@ -960,19 +960,12 @@ public class MWMActivity extends NvEventQueueActivity private void updateExternalStorageState() { - boolean available, writable; + boolean available = false, writable = false; final String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) - { available = writable = true; - } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) - { available = true; - writable = false; - } - else - available = writable = false; if (mStorageAvailable != available || mStorageWritable != writable) { @@ -1034,13 +1027,13 @@ public class MWMActivity extends NvEventQueueActivity } }; - mPathManager.StartExtStorageWatching(this, mExternalStorageReceiver); + registerReceiver(mExternalStorageReceiver, StoragePathManager.getMediaChangesIntentFilter()); updateExternalStorageState(); } private void stopWatchingExternalStorage() { - mPathManager.StopExtStorageWatching(); + mPathManager.stopExternalStorageWatching(); mExternalStorageReceiver = null; } diff --git a/android/src/com/mapswithme/maps/MWMApplication.java b/android/src/com/mapswithme/maps/MWMApplication.java index ba0f941389..a1ab5f64b9 100644 --- a/android/src/com/mapswithme/maps/MWMApplication.java +++ b/android/src/com/mapswithme/maps/MWMApplication.java @@ -17,6 +17,7 @@ import com.mapswithme.maps.bookmarks.data.BookmarkManager; import com.mapswithme.maps.guides.GuideInfo; import com.mapswithme.maps.guides.GuidesUtils; import com.mapswithme.maps.location.LocationService; +import com.mapswithme.util.Constants; import com.mapswithme.util.FbUtil; import com.mapswithme.util.Utils; import com.mapswithme.util.log.Logger; @@ -191,7 +192,7 @@ public class MWMApplication extends android.app.Application implements MapStorag public String getDataStoragePath() { - return Environment.getExternalStorageDirectory().getAbsolutePath() + "/MapsWithMe/"; + return Environment.getExternalStorageDirectory().getAbsolutePath() + Constants.MWM_DIR_POSTFIX; } public String getTempPath() diff --git a/android/src/com/mapswithme/maps/settings/SettingsActivity.java b/android/src/com/mapswithme/maps/settings/SettingsActivity.java index eb82db595e..83efc7a11c 100644 --- a/android/src/com/mapswithme/maps/settings/SettingsActivity.java +++ b/android/src/com/mapswithme/maps/settings/SettingsActivity.java @@ -30,7 +30,6 @@ import android.webkit.WebViewClient; import com.mapswithme.maps.MWMApplication; import com.mapswithme.maps.R; -import com.mapswithme.util.StoragePathManager; import com.mapswithme.util.UiUtils; import com.mapswithme.util.Yota; import com.mapswithme.util.statistics.Statistics; @@ -151,7 +150,7 @@ public class SettingsActivity extends PreferenceActivity PreferenceScreen screen = getPreferenceScreen(); if (Yota.isYota()) screen.removePreference(mStoragePreference); - else if (mPathManager.HasMoreThanOnceStorage()) + else if (mPathManager.hasMoreThanOneStorage()) screen.addPreference(mStoragePreference); else screen.removePreference(mStoragePreference); @@ -205,7 +204,7 @@ public class SettingsActivity extends PreferenceActivity storagePathSetup(); } }; - mPathManager.StartExtStorageWatching(this, receiver); + mPathManager.startExternalStorageWatching(this, receiver, null); storagePathSetup(); } @@ -213,7 +212,7 @@ public class SettingsActivity extends PreferenceActivity protected void onPause() { super.onPause(); - mPathManager.StopExtStorageWatching(); + mPathManager.stopExternalStorageWatching(); } @Override diff --git a/android/src/com/mapswithme/maps/settings/StoragePathActivity.java b/android/src/com/mapswithme/maps/settings/StoragePathActivity.java index 85ca0c5f6a..d8222367df 100644 --- a/android/src/com/mapswithme/maps/settings/StoragePathActivity.java +++ b/android/src/com/mapswithme/maps/settings/StoragePathActivity.java @@ -1,25 +1,21 @@ package com.mapswithme.maps.settings; -import android.os.Bundle; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; import android.view.View; import android.widget.ListView; import com.mapswithme.maps.base.MapsWithMeBaseListActivity; -import com.mapswithme.util.StoragePathManager; -public class StoragePathActivity extends MapsWithMeBaseListActivity +public class StoragePathActivity extends MapsWithMeBaseListActivity implements StoragePathManager.SetStoragePathListener { - private StoragePathManager m_pathManager = new StoragePathManager(); + private StoragePathManager mPathManager = new StoragePathManager(); + private StoragePathAdapter mAdapter; - private StoragePathManager.StoragePathAdapter getAdapter() + private StoragePathAdapter getAdapter() { - return (StoragePathManager.StoragePathAdapter) getListView().getAdapter(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); + return (StoragePathAdapter) getListView().getAdapter(); } @Override @@ -34,14 +30,43 @@ public class StoragePathActivity extends MapsWithMeBaseListActivity protected void onResume() { super.onResume(); - m_pathManager.StartExtStorageWatching(this, null); - setListAdapter(m_pathManager.GetAdapter()); + BroadcastReceiver receiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + if (mAdapter != null) + mAdapter.updateList(mPathManager.getStorageItems(), mPathManager.getCurrentStorageIndex(), StoragePathManager.getMwmDirSize()); + } + }; + mPathManager.startExternalStorageWatching(this, receiver, this); + initAdapter(); + mAdapter.updateList(mPathManager.getStorageItems(), mPathManager.getCurrentStorageIndex(), StoragePathManager.getMwmDirSize()); + setListAdapter(mAdapter); } @Override protected void onPause() { super.onPause(); - m_pathManager.StopExtStorageWatching(); + mPathManager.stopExternalStorageWatching(); + } + + private void initAdapter() + { + if (mAdapter == null) + mAdapter = new StoragePathAdapter(mPathManager, this); + } + + @Override + public void moveFilesFinished(String newPath) + { + mAdapter.updateList(mPathManager.getStorageItems(), mPathManager.getCurrentStorageIndex(), StoragePathManager.getMwmDirSize()); + } + + @Override + public void moveFilesFailed() + { + // } } diff --git a/android/src/com/mapswithme/maps/settings/StoragePathAdapter.java b/android/src/com/mapswithme/maps/settings/StoragePathAdapter.java new file mode 100644 index 0000000000..97ec2ca43b --- /dev/null +++ b/android/src/com/mapswithme/maps/settings/StoragePathAdapter.java @@ -0,0 +1,188 @@ +package com.mapswithme.maps.settings; + +import android.app.Activity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.CheckedTextView; +import android.widget.TextView; + +import com.mapswithme.maps.R; +import com.mapswithme.util.Utils; + +import java.util.ArrayList; +import java.util.List; + +class StoragePathAdapter extends BaseAdapter +{ + private static final int TYPE_HEADER = 0; + private static final int TYPE_ITEM = 1; + private static final int HEADERS_COUNT = 1; + private static final int TYPES_COUNT = 2; + + private StoragePathManager mStoragePathManager; + private final LayoutInflater mInflater; + private final Activity mContext; + private final int mListItemHeight; + + private List mItems; + private int mCurrent = -1; + private long mSizeNeeded; + + public StoragePathAdapter(StoragePathManager storagePathManager, Activity context) + { + mStoragePathManager = storagePathManager; + mContext = context; + mInflater = mContext.getLayoutInflater(); + + mListItemHeight = (int) Utils.getAttributeDimension(context, android.R.attr.listPreferredItemHeight); + } + + @Override + public int getItemViewType(int position) + { + return (position == 0 ? TYPE_HEADER : TYPE_ITEM); + } + + @Override + public int getViewTypeCount() + { + return TYPES_COUNT; + } + + @Override + public int getCount() + { + return (mItems != null ? mItems.size() + HEADERS_COUNT : HEADERS_COUNT); + } + + @Override + public StorageItem getItem(int position) + { + return (position == 0 ? null : mItems.get(getIndexFromPos(position))); + } + + @Override + public long getItemId(int position) + { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) + { + // 1. It's a strange thing, but when I tried to use setClickable, + // all the views become nonclickable. + // 2. I call setMinimumHeight(listPreferredItemHeight) + // because standard item's height is unknown. + + switch (getItemViewType(position)) + { + case TYPE_HEADER: + if (convertView == null) + { + convertView = mInflater.inflate(android.R.layout.simple_list_item_1, parent, false); + convertView.setMinimumHeight(mListItemHeight); + } + + final TextView v = (TextView) convertView; + v.setText(mContext.getString(R.string.maps) + ": " + getSizeString(mSizeNeeded)); + break; + case TYPE_ITEM: + final int index = getIndexFromPos(position); + final StorageItem item = mItems.get(index); + + if (convertView == null) + { + convertView = mInflater.inflate(android.R.layout.simple_list_item_single_choice, parent, false); + convertView.setMinimumHeight(mListItemHeight); + } + + final CheckedTextView ctv = (CheckedTextView) convertView; + ctv.setText(item.mPath + ": " + getSizeString(item.mSize)); + ctv.setChecked(index == mCurrent); + ctv.setEnabled((index == mCurrent) || isAvailable(index)); + break; + } + + return convertView; + } + + public void onItemClick(int position) + { + final int index = getIndexFromPos(position); + if (isAvailable(index)) + mStoragePathManager.onStorageItemClick(index); + } + + public void updateList(ArrayList items, int currentItemIndex, long dirSize) + { + mSizeNeeded = dirSize; + mItems = items; + mCurrent = currentItemIndex; + + notifyDataSetChanged(); + } + + private String getSizeString(long size) + { + final String arrS[] = {"Kb", "Mb", "Gb"}; + + long current = 1024; + int i = 0; + for (; i < arrS.length; ++i) + { + final long bound = 1024 * current; + if (size < bound) + break; + else + current = bound; + } + + // left 1 digit after the comma and add postfix string + return String.format("%.1f %s", (double) size / (double) current, arrS[i]); + } + + private boolean isAvailable(int index) + { + assert (index >= 0 && index < mItems.size()); + return ((mCurrent != index) && (mItems.get(index).mSize >= mSizeNeeded)); + } + + private int getIndexFromPos(int position) + { + final int index = position - HEADERS_COUNT; + assert (index >= 0 && index < mItems.size()); + return index; + } + + public static class StorageItem + { + public final String mPath; + public final long mSize; + + StorageItem(String path, long size) + { + mPath = path; + mSize = size; + } + + @Override + public boolean equals(Object o) + { + if (o == this) + return true; + if (o == null || !(o instanceof StorageItem)) + return false; + StorageItem other = (StorageItem) o; + return mSize == other.mSize && mPath.equals(other.mPath); + } + + @Override + public int hashCode() + { + return Long.valueOf(mSize).hashCode(); + } + } +} diff --git a/android/src/com/mapswithme/maps/settings/StoragePathManager.java b/android/src/com/mapswithme/maps/settings/StoragePathManager.java new file mode 100644 index 0000000000..9eb2004ff9 --- /dev/null +++ b/android/src/com/mapswithme/maps/settings/StoragePathManager.java @@ -0,0 +1,606 @@ +package com.mapswithme.maps.settings; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; +import android.util.Log; + +import com.mapswithme.maps.Framework; +import com.mapswithme.maps.R; +import com.mapswithme.util.Constants; +import com.mapswithme.util.Utils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +public class StoragePathManager +{ + public interface SetStoragePathListener + { + void moveFilesFinished(String newPath); + + void moveFilesFailed(); + } + + private static String TAG = StoragePathManager.class.getName(); + + private static final int VOLD_MODE = 1; + private static final int MOUNTS_MODE = 2; + + private BroadcastReceiver mExternalReceiver; + private BroadcastReceiver mInternalReceiver; + private Activity mActivity; + private ArrayList mItems; + private int mCurrentStorageIndex = -1; + private SetStoragePathListener mStorageListener; + + /** + * Observes status of connected media and retrieves list of available external storages. + * @param activity context + * @param receiver receiver to get broadcasts of media events. can be null + * @param listener listener to get notifications about maps transfers. can be null + */ + public void startExternalStorageWatching(Activity activity, BroadcastReceiver receiver, SetStoragePathListener listener) + { + mActivity = activity; + mStorageListener = listener; + mExternalReceiver = receiver; + mInternalReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + if (mExternalReceiver != null) + mExternalReceiver.onReceive(context, intent); + + updateExternalStorages(); + } + }; + + mActivity.registerReceiver(mInternalReceiver, getMediaChangesIntentFilter()); + updateExternalStorages(); + } + + public static IntentFilter getMediaChangesIntentFilter() + { + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_MEDIA_MOUNTED); + filter.addAction(Intent.ACTION_MEDIA_REMOVED); + filter.addAction(Intent.ACTION_MEDIA_EJECT); + filter.addAction(Intent.ACTION_MEDIA_SHARED); + filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); + filter.addAction(Intent.ACTION_MEDIA_UNMOUNTABLE); + filter.addAction(Intent.ACTION_MEDIA_CHECKING); + filter.addAction(Intent.ACTION_MEDIA_NOFS); + filter.addDataScheme(Constants.Url.DATA_SCHEME_FILE); + + return filter; + } + + public void stopExternalStorageWatching() + { + if (mInternalReceiver != null) + { + mActivity.unregisterReceiver(mInternalReceiver); + mInternalReceiver = null; + mExternalReceiver = null; + } + } + + public boolean hasMoreThanOneStorage() + { + return mItems.size() > 1; + } + + public void updateExternalStorages() + { + ArrayList paths = new ArrayList(); + + if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) + { + File[] files = mActivity.getExternalFilesDirs(null); + if (files != null) + { + File primaryStorageDir = mActivity.getExternalFilesDir(null); + for (File f : files) + { + // On kitkat and Greater we ignore private folder on primary storage + // like "PrimaryStorage/Android/data/com.mapswithme.maps.pro/file/" + // because we can write to the root of PrimaryStorage/ + if (f != null && !f.equals(primaryStorageDir)) + paths.add(f.getPath()); + } + } + } + + parseMountFiles(paths); + + Map pathsSizesMap = new HashMap(); + for (String path : paths) + addStoragePathWithSize(path, pathsSizesMap); + addStoragePathWithSize(Environment.getExternalStorageDirectory().getAbsolutePath(), pathsSizesMap); + String currentStorageDir = getWritableDirRoot(); + addStoragePathWithSize(currentStorageDir, pathsSizesMap); + + mItems = new ArrayList(); + mCurrentStorageIndex = -1; + for (Map.Entry entry : pathsSizesMap.entrySet()) + { + StoragePathAdapter.StorageItem item = new StoragePathAdapter.StorageItem(entry.getValue(), entry.getKey()); + mItems.add(item); + if (item.mPath.equals(currentStorageDir)) + mCurrentStorageIndex = mItems.size() - 1; + } + } + + public ArrayList getStorageItems() + { + return mItems; + } + + public int getCurrentStorageIndex() + { + return mCurrentStorageIndex; + } + + private static void parseMountFiles(ArrayList paths) + { + parseMountFile("/etc/vold.conf", VOLD_MODE, paths); + parseMountFile("/etc/vold.fstab", VOLD_MODE, paths); + parseMountFile("/system/etc/vold.fstab", VOLD_MODE, paths); + parseMountFile("/proc/mounts", MOUNTS_MODE, paths); + } + + /// @name Assume that MapsWithMe folder doesn't have inner folders and symbolic links. + public static long getMwmDirSize() + { + final File dir = new File(Framework.nativeGetWritableDir()); + assert (dir.exists()); + assert (dir.isDirectory()); + + final File[] files = dir.listFiles(); + if (files == null) + return 0; + + long size = 0; + for (final File f : files) + { + assert (f.isFile()); + size += f.length(); + } + + return size; + } + + public boolean moveBookmarks() + { + ArrayList pathes = new ArrayList(); + if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) + parseMountFiles(pathes); + + ArrayList approvedPathes = new ArrayList(); + for (String path : pathes) + { + String mwmPath = path + Constants.MWM_DIR_POSTFIX; + File f = new File(mwmPath); + if (f.exists() || f.canRead() || f.isDirectory()) + approvedPathes.add(mwmPath); + } + final String settingsDir = Framework.nativeGetSettingsDir(); + final String writableDir = Framework.nativeGetWritableDir(); + final String bookmarkDir = Framework.nativeGetBookmarkDir(); + final String bookmarkFileExt = Framework.nativeGetBookmarksExt(); + + LinkedHashSet bookmarks = new LinkedHashSet(); + if (!settingsDir.equals(writableDir)) + approvedPathes.add(writableDir); + + for (String path : approvedPathes) + { + if (!path.equals(settingsDir)) + accumulateFiles(path, bookmarkFileExt, bookmarks); + } + + long bookmarksSize = 0; + for (File f : bookmarks) + bookmarksSize += f.length(); + + if (getFreeBytesAtPath(bookmarkDir) < bookmarksSize) + return false; + + for (File f : bookmarks) + { + String name = f.getName(); + name = name.replace(bookmarkFileExt, ""); + name = nativeGenerateUniqueBookmarkName(name); + try + { + copyFile(f, new File(name)); + f.delete(); + } catch (IOException e) + { + return false; + } + } + + Framework.nativeLoadbookmarks(); + + return true; + } + + private static void accumulateFiles(final String dirPath, final String filesExtension, Set result) + { + File f = new File(dirPath); + File[] bookmarks = f.listFiles(new FileFilter() + { + @Override + public boolean accept(File pathname) + { + return pathname.getName().endsWith(filesExtension); + } + }); + + result.addAll(Arrays.asList(bookmarks)); + } + + protected void onStorageItemClick(int index) + { + final StoragePathAdapter.StorageItem oldItem = (mCurrentStorageIndex != -1) ? mItems.get(mCurrentStorageIndex) : null; + final StoragePathAdapter.StorageItem item = mItems.get(index); + final String path = getItemFullPath(item); + + final File f = new File(path); + if (!f.exists() && !f.mkdirs()) + { + Log.e(TAG, "Can't create directory: " + path); + return; + } + + new AlertDialog.Builder(mActivity).setCancelable(false).setTitle(R.string.move_maps) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dlg, int which) + { + setStoragePath(mActivity, new StoragePathManager.SetStoragePathListener() + { + @Override + public void moveFilesFinished(String newPath) + { + updateExternalStorages(); + if (mStorageListener != null) + mStorageListener.moveFilesFinished(newPath); + } + + @Override + public void moveFilesFailed() + { + updateExternalStorages(); + if (mStorageListener != null) + mStorageListener.moveFilesFailed(); + } + }, item, oldItem, R.string.wait_several_minutes); + + dlg.dismiss(); + } + }).setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dlg, int which) + { + dlg.dismiss(); + } + }).create().show(); + } + + public void checkWritableDir(Context context, SetStoragePathListener listener) + { + if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT) + return; + + final String settingsDir = Framework.nativeGetSettingsDir(); + final String writableDir = Framework.nativeGetWritableDir(); + + if (settingsDir.equals(writableDir)) + return; + + if (isDirWritable(writableDir)) + return; + + final long size = getMwmDirSize(); + for (StoragePathAdapter.StorageItem item : mItems) + { + if (item.mSize > size) + { + setStoragePath(context, listener, item, new StoragePathAdapter.StorageItem(getWritableDirRoot(), 0), + R.string.kitkat_optimization_in_progress); + return; + } + } + + listener.moveFilesFailed(); + } + + private void setStoragePath(Context context, SetStoragePathListener listener, StoragePathAdapter.StorageItem newStorage, + StoragePathAdapter.StorageItem oldStorage, int messageId) + { + MoveFilesTask task = new MoveFilesTask(context, listener, newStorage, oldStorage, messageId); + task.execute(""); + } + + private static boolean doMoveMaps(StoragePathAdapter.StorageItem newStorage, StoragePathAdapter.StorageItem oldStorage) + { + String fullOldPath = getItemFullPath(oldStorage); + String fullNewPath = getItemFullPath(newStorage); + File oldDir = new File(fullOldPath); + File newDir = new File(fullNewPath); + if (!newDir.exists()) + newDir.mkdir(); + + assert (isDirWritable(fullNewPath)); + assert (newDir.isDirectory()); + assert (oldDir.isDirectory()); + + final String[] extensions = Framework.nativeGetMovableFilesExt(); + + File[] internalFiles = oldDir.listFiles(new FileFilter() + { + + @Override + public boolean accept(File pathname) + { + for (String postfix : extensions) + { + if (pathname.getName().endsWith(postfix)) + return true; + } + return false; + } + }); + + try + { + for (File moveFile : internalFiles) + copyFile(moveFile, new File(fullNewPath + moveFile.getName())); + } catch (IOException e) + { + for (File moveFile : internalFiles) + new File(fullNewPath + moveFile.getName()).delete(); + return false; + } + + Framework.nativeSetWritableDir(fullNewPath); + + for (File moveFile : internalFiles) + moveFile.delete(); + + return true; + } + + private static void copyFile(File source, File dest) throws IOException + { + FileChannel inputChannel = null; + FileChannel outputChannel = null; + try + { + inputChannel = new FileInputStream(source).getChannel(); + outputChannel = new FileOutputStream(dest).getChannel(); + outputChannel.transferFrom(inputChannel, 0, inputChannel.size()); + } finally + { + inputChannel.close(); + outputChannel.close(); + } + } + + private class MoveFilesTask extends AsyncTask + { + private final ProgressDialog mDlg; + private final StoragePathAdapter.StorageItem mNewStorage; + private final StoragePathAdapter.StorageItem mOldStorage; + private final SetStoragePathListener mListener; + + public MoveFilesTask(Context context, SetStoragePathListener listener, StoragePathAdapter.StorageItem newStorage, + StoragePathAdapter.StorageItem oldStorage, int messageID) + { + mNewStorage = newStorage; + mOldStorage = oldStorage; + mListener = listener; + + mDlg = new ProgressDialog(context); + mDlg.setMessage(context.getString(messageID)); + mDlg.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mDlg.setIndeterminate(true); + mDlg.setCancelable(false); + } + + @Override + protected void onPreExecute() + { + mDlg.show(); + } + + @Override + protected Boolean doInBackground(String... params) + { + return doMoveMaps(mNewStorage, mOldStorage); + } + + @Override + protected void onPostExecute(Boolean result) + { + // Using dummy try-catch because of the following: + // http://stackoverflow.com/questions/2745061/java-lang-illegalargumentexception-view-not-attached-to-window-manager + try + { + mDlg.dismiss(); + } catch (final Exception e) + { + e.printStackTrace(); + } + + if (result) + mListener.moveFilesFinished(mNewStorage.mPath); + else + mListener.moveFilesFailed(); + + updateExternalStorages(); + } + } + + private static void addStoragePathWithSize(String path, Map sizesPaths) + { + Log.d(TAG, "trying to add path " + path); + try + { + final File f = new File(path + "/"); + if (f.exists() && f.isDirectory() && f.canWrite() && isDirWritable(path)) + { + final long size = getFreeBytesAtPath(path); + + if (size > 0) + { + Log.d(TAG, "add path " + path + ", size = " + size); + sizesPaths.put(size, path); + return; + } + } + } catch (final IllegalArgumentException ex) + { + // Suppress exceptions for unavailable storages. + Log.i(TAG, "StatFs error for storage: " + path); + } + } + + // http://stackoverflow.com/questions/8151779/find-sd-card-volume-label-on-android + // http://stackoverflow.com/questions/5694933/find-an-external-sd-card-location + // http://stackoverflow.com/questions/14212969/file-canwrite-returns-false-on-some-devices-although-write-external-storage-pe + private static void parseMountFile(String file, int mode, ArrayList paths) + { + Log.i(TAG, "Parsing " + file); + + BufferedReader reader = null; + try + { + reader = new BufferedReader(new FileReader(file)); + + while (true) + { + final String line = reader.readLine(); + Log.i(TAG, "line : " + line); + if (line == null) + break; + + // standard regexp for all possible whitespaces (space, tab, etc) + final String[] arr = line.split("\\s+"); + + // split may return empty first strings + int start = 0; + while (start < arr.length && arr[start].length() == 0) + ++start; + + if (arr.length - start > 3) + { + if (arr[start].charAt(0) == '#') + continue; + + if (mode == VOLD_MODE) + { + if (arr[start].startsWith("dev_mount")) + paths.add(arr[start + 2]); + } + else + { + assert (mode == MOUNTS_MODE); + final String prefixes[] = {"tmpfs", "/dev/block/vold", "/dev/fuse", "/mnt/media_rw"}; + for (final String s : prefixes) + if (arr[start].startsWith(s)) + paths.add(arr[start + 1]); + } + } + } + } catch (final IOException e) + { + Log.w(TAG, "Can't read file: " + file); + } finally + { + Utils.closeStream(reader); + } + } + + @SuppressWarnings("deprecation") + private static long getFreeBytesAtPath(String path) + { + long size = 0; + try + { + if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.GINGERBREAD) + { + final StatFs stat = new StatFs(path); + size = stat.getAvailableBlocks() * (long) stat.getBlockSize(); + } + else + size = new File(path).getFreeSpace(); + } catch (RuntimeException e) + { + e.printStackTrace(); + } + + return size; + } + + // we can't only call canWrite, because on KitKat (Samsung S4) this return + // true for some directories on sdcard but actually it's read only + // see https://code.google.com/p/android/issues/detail?id=66369 for details + private static boolean isDirWritable(String path) + { + final File f = new File(path + "/testDir"); + f.mkdir(); + if (f.exists()) + { + f.delete(); + return true; + } + + return false; + } + + private static String getItemFullPath(StoragePathAdapter.StorageItem item) + { + return item.mPath + Constants.MWM_DIR_POSTFIX; + } + + private static String getWritableDirRoot() + { + String writableDir = Framework.nativeGetWritableDir(); + int index = writableDir.lastIndexOf(Constants.MWM_DIR_POSTFIX); + if (index != -1) + writableDir = writableDir.substring(0, index); + + return writableDir; + } + + private static native String nativeGenerateUniqueBookmarkName(String baseName); +} diff --git a/android/src/com/mapswithme/util/Constants.java b/android/src/com/mapswithme/util/Constants.java index ba02d81d1d..ca1f9e315b 100644 --- a/android/src/com/mapswithme/util/Constants.java +++ b/android/src/com/mapswithme/util/Constants.java @@ -2,12 +2,8 @@ package com.mapswithme.util; public class Constants { - private Constants() {} - public static class Url { - private Url() {} - public static final String GE0_PREFIX = "ge0://"; public static final String HTTP_GE0_PREFIX = "http://ge0.me/"; @@ -18,8 +14,13 @@ public class Constants // Profile id is taken from http://graph.facebook.com/MapsWithMe public static final String FB_MAPSME_COMMUNITY_NATIVE = "fb://profile/111923085594432"; + public static final String DATA_SCHEME_FILE = "file"; + + private Url() {} } public static final String FB_PACKAGE = "com.facebook.katana"; + public static final String MWM_DIR_POSTFIX = "/MapsWithMe/"; + private Constants() {} } diff --git a/android/src/com/mapswithme/util/StoragePathManager.java b/android/src/com/mapswithme/util/StoragePathManager.java deleted file mode 100644 index d64ca8e04b..0000000000 --- a/android/src/com/mapswithme/util/StoragePathManager.java +++ /dev/null @@ -1,794 +0,0 @@ -package com.mapswithme.util; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.ProgressDialog; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Environment; -import android.os.StatFs; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.CheckedTextView; -import android.widget.TextView; - -import com.mapswithme.maps.Framework; -import com.mapswithme.maps.R; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -public class StoragePathManager -{ - public static class StorageItem - { - public final String mPath; - public final long mSize; - - StorageItem(String path, long size) - { - mPath = path; - mSize = size; - } - - @Override - public boolean equals(Object o) - { - if (o == this) - return true; - if (o == null) - return false; - StorageItem other = (StorageItem) o; - return mSize == other.mSize || mPath.equals(other.mPath); - } - - @Override - public int hashCode() - { - return Long.valueOf(mSize).hashCode(); - } - } - - public interface SetStoragePathListener - { - void MoveFilesFinished(String newPath); - - void MoveFilesFailed(); - } - - abstract public static class StoragePathAdapter extends BaseAdapter - { - abstract public void onItemClick(int position); - } - - // / ListView adapter - private class StoragePathAdapterImpl extends StoragePathAdapter - { - // / @name Different row types. - // @{ - private static final int TYPE_HEADER = 0; - private static final int TYPE_ITEM = 1; - private static final int HEADERS_COUNT = 1; - private static final int TYPES_COUNT = 2; - // @} - - private final LayoutInflater mInflater; - private final Activity mContext; - private final int mListItemHeight; - - private List mItems = null; - private int mCurrent = -1; - private long mSizeNeeded; - - public StoragePathAdapterImpl(Activity context) - { - mContext = context; - mInflater = mContext.getLayoutInflater(); - - mListItemHeight = (int) Utils.getAttributeDimension(context, android.R.attr.listPreferredItemHeight); - } - - @Override - public int getItemViewType(int position) - { - return (position == 0 ? TYPE_HEADER : TYPE_ITEM); - } - - @Override - public int getViewTypeCount() - { - return TYPES_COUNT; - } - - @Override - public int getCount() - { - return (mItems != null ? mItems.size() + HEADERS_COUNT : HEADERS_COUNT); - } - - @Override - public StorageItem getItem(int position) - { - return (position == 0 ? null : mItems.get(getIndexFromPos(position))); - } - - @Override - public long getItemId(int position) - { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) - { - // 1. It's a strange thing, but when I tried to use setClickable, - // all the views become nonclickable. - // 2. I call setMinimumHeight(listPreferredItemHeight) - // because standard item's height is unknown. - - switch (getItemViewType(position)) - { - case TYPE_HEADER: - if (convertView == null) - { - convertView = mInflater.inflate(android.R.layout.simple_list_item_1, null); - convertView.setMinimumHeight(mListItemHeight); - } - - final TextView v = (TextView) convertView; - v.setText(mContext.getString(R.string.maps) + ": " + getSizeString(mSizeNeeded)); - break; - - case TYPE_ITEM: - final int index = getIndexFromPos(position); - final StorageItem item = mItems.get(index); - - if (convertView == null) - { - convertView = mInflater.inflate(android.R.layout.simple_list_item_single_choice, null); - convertView.setMinimumHeight(mListItemHeight); - } - - final CheckedTextView ctv = (CheckedTextView) convertView; - ctv.setText(item.mPath + ": " + getSizeString(item.mSize)); - ctv.setChecked(index == mCurrent); - ctv.setEnabled((index == mCurrent) || isAvailable(index)); - break; - } - - return convertView; - } - - @Override - public void onItemClick(int position) - { - final int index = getIndexFromPos(position); - if (isAvailable(index)) - onStorageItemClick(index); - } - - public void updateList(ArrayList items, int currentItemIndex, long dirSize) - { - mSizeNeeded = dirSize; - mItems = items; - mCurrent = currentItemIndex; - - notifyDataSetChanged(); - } - - @SuppressLint("DefaultLocale") - private String getSizeString(long size) - { - final String arrS[] = {"Kb", "Mb", "Gb"}; - - long current = 1024; - int i = 0; - for (; i < arrS.length; ++i) - { - final long bound = 1024 * current; - if (size < bound) - break; - else - current = bound; - } - - // left 1 digit after the comma and add postfix string - return String.format("%.1f %s", (double) size / (double) current, arrS[i]); - } - - private boolean isAvailable(int index) - { - assert (index >= 0 && index < mItems.size()); - return ((mCurrent != index) && (mItems.get(index).mSize >= mSizeNeeded)); - } - - private int getIndexFromPos(int position) - { - final int index = position - HEADERS_COUNT; - assert (index >= 0 && index < mItems.size()); - return index; - } - } - - private static String TAG = "StoragePathManager"; - private static String MWM_DIR_POSTFIX = "/MapsWithMe/"; - - private BroadcastReceiver mExternalListener; - private BroadcastReceiver mInternalListener; - private Activity mContext = null; - private ArrayList mItems = null; - private StoragePathAdapterImpl mAdapter = null; - private int mCurrentItemIndex = -1; - - public void StartExtStorageWatching(Activity context, BroadcastReceiver listener) - { - mContext = context; - mExternalListener = listener; - - mInternalListener = new BroadcastReceiver() - { - @Override - public void onReceive(Context context, Intent intent) - { - if (mExternalListener != null) - mExternalListener.onReceive(context, intent); - - UpdateExternalStorages(); - } - }; - - final IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_MEDIA_MOUNTED); - filter.addAction(Intent.ACTION_MEDIA_REMOVED); - filter.addAction(Intent.ACTION_MEDIA_EJECT); - filter.addAction(Intent.ACTION_MEDIA_SHARED); - filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); - filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); - filter.addAction(Intent.ACTION_MEDIA_UNMOUNTABLE); - filter.addAction(Intent.ACTION_MEDIA_CHECKING); - filter.addAction(Intent.ACTION_MEDIA_NOFS); - filter.addDataScheme("file"); - - mContext.registerReceiver(mInternalListener, filter); - UpdateExternalStorages(); - } - - public StoragePathAdapter GetAdapter() - { - if (mAdapter == null) - { - mAdapter = new StoragePathAdapterImpl(mContext); - UpdateExternalStorages(); - } - - return mAdapter; - } - - public void StopExtStorageWatching() - { - if (mInternalListener != null) - { - mContext.unregisterReceiver(mInternalListener); - mInternalListener = null; - mExternalListener = null; - mAdapter = null; - } - } - - public boolean HasMoreThanOnceStorage() - { - return mItems.size() > 1; - } - - public void UpdateExternalStorages() - { - ArrayList pathes = new ArrayList(); - - if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) - { - File[] files = mContext.getExternalFilesDirs(null); - if (files != null) - { - File primaryStorageDir = mContext.getExternalFilesDir(null); - for (File f : files) - { - // On kitkat and Greater we ignore private folder on primary storage - // like "PrimaryStorage/Android/data/com.mapswithme.maps.pro/file/" - // because we can write to the root of PrimaryStorage/ - if (f != null && !f.equals(primaryStorageDir)) - pathes.add(f.getPath()); - } - } - } - - parseMountFiles(pathes); - - ArrayList items = new ArrayList(); - for (String path : pathes) - AddStorage(path, items); - - AddStorage(Environment.getExternalStorageDirectory().getAbsolutePath(), items); - - String writableDir = GetWritableDirRoot(); - - StorageItem currentItem = AddStorage(writableDir, items); - LinkedHashSet itemsSet = new LinkedHashSet(items); - if (currentItem != null) - itemsSet.remove(currentItem); - - mItems = new ArrayList(itemsSet); - if (currentItem != null) - { - mItems.add(0, currentItem); - mCurrentItemIndex = mItems.indexOf(currentItem); - } - else - mCurrentItemIndex = -1; - if (mAdapter != null) - mAdapter.updateList(mItems, mCurrentItemIndex, GetMWMDirSize()); - } - - private void parseMountFiles(ArrayList pathes) - { - ParseMountFile("/etc/vold.conf", VOLD_MODE, pathes); - ParseMountFile("/etc/vold.fstab", VOLD_MODE, pathes); - ParseMountFile("/system/etc/vold.fstab", VOLD_MODE, pathes); - ParseMountFile("/proc/mounts", MOUNTS_MODE, pathes); - } - - /// @name Assume that MapsWithMe folder doesn't have inner folders and symbolic links. - public long GetMWMDirSize() - { - final File dir = new File(Framework.nativeGetWritableDir()); - assert (dir.exists()); - assert (dir.isDirectory()); - - final File[] files = dir.listFiles(); - if (files == null) - return 0; - - long size = 0; - for (final File f : files) - { - assert (f.isFile()); - size += f.length(); - } - - return size; - } - - public boolean MoveBookmarks() - { - ArrayList pathes = new ArrayList(); - if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) - parseMountFiles(pathes); - - ArrayList approvedPathes = new ArrayList(); - for (String path : pathes) - { - String mwmPath = path + MWM_DIR_POSTFIX; - File f = new File(mwmPath); - if (f.exists() || f.canRead() || f.isDirectory()) - approvedPathes.add(mwmPath); - } - final String settingsDir = Framework.nativeGetSettingsDir(); - final String writableDir = Framework.nativeGetWritableDir(); - final String bookmarkDir = Framework.nativeGetBookmarkDir(); - final String bookmarkFileExt = Framework.nativeGetBookmarksExt(); - - LinkedHashSet bookmarks = new LinkedHashSet(); - if (!settingsDir.equals(writableDir)) - approvedPathes.add(writableDir); - - for (String path : approvedPathes) - { - if (!path.equals(settingsDir)) - AccamulateFiles(path, bookmarkFileExt, bookmarks); - } - - long bookmarksSize = 0; - for (File f : bookmarks) - bookmarksSize += f.length(); - - if (GetFreeBytesAtPath(bookmarkDir) < bookmarksSize) - return false; - - for (File f : bookmarks) - { - String name = f.getName(); - name = name.replace(bookmarkFileExt, ""); - name = nativeGenerateUniqueBookmarkName(name); - try - { - copyFile(f, new File(name)); - f.delete(); - } catch (IOException e) - { - return false; - } - } - - Framework.nativeLoadbookmarks(); - - return true; - } - - private void AccamulateFiles(final String dirPath, final String filesExtension, Set result) - { - File f = new File(dirPath); - File[] bookmarks = f.listFiles(new FileFilter() - { - @Override - public boolean accept(File pathname) - { - return pathname.getName().endsWith(filesExtension); - } - }); - - result.addAll(Arrays.asList(bookmarks)); - } - - private void onStorageItemClick(int index) - { - final StorageItem oldItem = (mCurrentItemIndex != -1) ? mItems.get(mCurrentItemIndex) : null; - final StorageItem item = mItems.get(index); - final String path = GetItemFullPath(item); - - final File f = new File(path); - if (!f.exists() && !f.mkdirs()) - { - Log.e(TAG, "Can't create directory: " + path); - return; - } - - new AlertDialog.Builder(mContext).setCancelable(false).setTitle(R.string.move_maps) - .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dlg, int which) - { - SetStoragePathImpl(mContext, new StoragePathManager.SetStoragePathListener() - { - @Override - public void MoveFilesFinished(String newPath) - { - UpdateExternalStorages(); - } - - @Override - public void MoveFilesFailed() - { - UpdateExternalStorages(); - } - }, item, oldItem, R.string.wait_several_minutes); - - dlg.dismiss(); - } - }).setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dlg, int which) - { - dlg.dismiss(); - } - }).create().show(); - } - - public void CheckWritableDir(Context context, SetStoragePathListener listener) - { - if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT) - return; - - final String settingsDir = Framework.nativeGetSettingsDir(); - final String writableDir = Framework.nativeGetWritableDir(); - - if (settingsDir.equals(writableDir)) - return; - - if (IsDirWritable(writableDir)) - return; - - final long size = GetMWMDirSize(); - for (StorageItem item : mItems) - { - if (item.mSize > size) - { - SetStoragePathImpl(context, listener, item, new StorageItem(GetWritableDirRoot(), 0), - R.string.kitkat_optimization_in_progress); - return; - } - } - - listener.MoveFilesFailed(); - } - - private void SetStoragePathImpl(Context context, SetStoragePathListener listener, StorageItem newStorage, - StorageItem oldStorage, int messageId) - { - MoveFilesTask task = new MoveFilesTask(context, listener, newStorage, oldStorage, messageId); - task.execute(""); - } - - static private boolean DoMoveMaps(StorageItem newStorage, StorageItem oldStorage) - { - String fullOldPath = GetItemFullPath(oldStorage); - String fullNewPath = GetItemFullPath(newStorage); - File oldDir = new File(fullOldPath); - File newDir = new File(fullNewPath); - if (!newDir.exists()) - newDir.mkdir(); - - assert (IsDirWritable(fullNewPath)); - assert (newDir.isDirectory()); - assert (oldDir.isDirectory()); - - final String[] extensions = Framework.nativeGetMovableFilesExt(); - - File[] internalFiles = oldDir.listFiles(new FileFilter() - { - - @Override - public boolean accept(File pathname) - { - for (String postfix : extensions) - { - if (pathname.getName().endsWith(postfix)) - return true; - } - return false; - } - }); - - try - { - for (File moveFile : internalFiles) - copyFile(moveFile, new File(fullNewPath + moveFile.getName())); - } catch (IOException e) - { - for (File moveFile : internalFiles) - new File(fullNewPath + moveFile.getName()).delete(); - return false; - } - - Framework.nativeSetWritableDir(fullNewPath); - - for (File moveFile : internalFiles) - moveFile.delete(); - - return true; - } - - private static void copyFile(File source, File dest) throws IOException - { - FileChannel inputChannel = null; - FileChannel outputChannel = null; - try - { - inputChannel = new FileInputStream(source).getChannel(); - outputChannel = new FileOutputStream(dest).getChannel(); - outputChannel.transferFrom(inputChannel, 0, inputChannel.size()); - } finally - { - inputChannel.close(); - outputChannel.close(); - } - } - - private class MoveFilesTask extends AsyncTask - { - private final ProgressDialog mDlg; - private final StorageItem mNewStorage; - private final StorageItem mOldStorage; - private final SetStoragePathListener mListener; - - public MoveFilesTask(Context context, SetStoragePathListener listener, StorageItem newStorage, - StorageItem oldStorage, int messageID) - { - mNewStorage = newStorage; - mOldStorage = oldStorage; - mListener = listener; - - mDlg = new ProgressDialog(context); - mDlg.setMessage(context.getString(messageID)); - mDlg.setProgressStyle(ProgressDialog.STYLE_SPINNER); - mDlg.setIndeterminate(true); - mDlg.setCancelable(false); - } - - @Override - protected void onPreExecute() - { - mDlg.show(); - } - - @Override - protected Boolean doInBackground(String... params) - { - return DoMoveMaps(mNewStorage, mOldStorage); - } - - @Override - protected void onPostExecute(Boolean result) - { - // Using dummy try-catch because of the following: - // http://stackoverflow.com/questions/2745061/java-lang-illegalargumentexception-view-not-attached-to-window-manager - try - { - mDlg.dismiss(); - } catch (final Exception e) - {} - - if (result) - mListener.MoveFilesFinished(mNewStorage.mPath); - else - mListener.MoveFilesFailed(); - - UpdateExternalStorages(); - } - } - - static private StorageItem AddStorage(String path, ArrayList items) - { - try - { - final File f = new File(path + "/"); - if (f.exists() && f.isDirectory() && f.canWrite()) - { - if (!IsDirWritable(path)) - return null; - - final long size = GetFreeBytesAtPath(path); - - if (size > 0) - { - final StorageItem item = new StorageItem(path, size); - items.add(item); - return item; - } - } - } catch (final IllegalArgumentException ex) - { - // Suppress exceptions for unavailable storages. - Log.i(TAG, "StatFs error for storage: " + path); - } - - return null; - } - - private static int VOLD_MODE = 1; - private static int MOUNTS_MODE = 2; - - // http://stackoverflow.com/questions/8151779/find-sd-card-volume-label-on-android - // http://stackoverflow.com/questions/5694933/find-an-external-sd-card-location - // http://stackoverflow.com/questions/14212969/file-canwrite-returns-false-on-some-devices-although-write-external-storage-pe - private static void ParseMountFile(String file, int mode, ArrayList pathes) - { - Log.i(TAG, "Parsing " + file); - - BufferedReader reader = null; - try - { - reader = new BufferedReader(new FileReader(file)); - - while (true) - { - final String line = reader.readLine(); - if (line == null) - break; - - // standard regexp for all possible whitespaces (space, tab, etc) - final String[] arr = line.split("\\s+"); - - // split may return empty first strings - int start = 0; - while (start < arr.length && arr[start].length() == 0) - ++start; - - if (arr.length - start > 3) - { - if (arr[start + 0].charAt(0) == '#') - continue; - - if (mode == VOLD_MODE) - { - Log.i(TAG, "Label = " + arr[start + 1] + "; Path = " + arr[start + 2]); - - if (arr[start + 0].startsWith("dev_mount")) - pathes.add(arr[start + 2]); - } - else - { - assert (mode == MOUNTS_MODE); - Log.i(TAG, "Label = " + arr[start + 0] + "; Path = " + arr[start + 1]); - - final String prefixes[] = {"tmpfs", "/dev/block/vold", "/dev/fuse", "/mnt/media_rw"}; - for (final String s : prefixes) - if (arr[start + 0].startsWith(s)) - pathes.add(arr[start + 1]); - } - } - } - } catch (final IOException e) - { - Log.w(TAG, "Can't read file: " + file); - } finally - { - Utils.closeStream(reader); - } - } - - @SuppressWarnings("deprecation") - static private long GetFreeBytesAtPath(String path) - { - long size = 0; - try - { - if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.GINGERBREAD) - { - final StatFs stat = new StatFs(path); - size = stat.getAvailableBlocks() * (long) stat.getBlockSize(); - } - else - size = new File(path).getFreeSpace(); - } catch (RuntimeException e) - { - Log.d(TAG, e.getMessage()); - Log.d(TAG, e.getStackTrace().toString()); - } - - return size; - } - - static private boolean IsDirWritable(String path) - { - final File f = new File(path + "/testDir"); - f.mkdir(); - // we can't only call canWrite, because on KitKat (Samsung S4) this return - // true - // for sdcard but actually it's read only - if (f.exists()) - { - f.delete(); - return true; - } - - return false; - } - - static private String GetItemFullPath(StorageItem item) - { - return item.mPath + MWM_DIR_POSTFIX; - } - - static private String GetWritableDirRoot() - { - String writableDir = Framework.nativeGetWritableDir(); - int index = writableDir.lastIndexOf(MWM_DIR_POSTFIX); - if (index != -1) - writableDir = writableDir.substring(0, index); - - return writableDir; - } - - static private native String nativeGenerateUniqueBookmarkName(String baseName); -}