[android] Rewrite SettingsActivity to use custom ListAdapter and AsyncTask.

This commit is contained in:
vng 2012-10-02 18:53:47 +03:00 committed by Alex Zolotarev
parent 37e7594b0f
commit b28457b29b
2 changed files with 427 additions and 244 deletions

View file

@ -7,219 +7,468 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.StatFs;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckedTextView;
import android.widget.ListView;
import android.widget.TextView;
import com.mapswithme.util.Utils;
public class SettingsActivity extends ListActivity
{
private static String TAG = "SettingsActivity";
private static String MWM_DIR_POSTFIX = "/MapsWithMe/";
private List<String> m_choices = new ArrayList<String>();
private List<String> m_pathes = new ArrayList<String>();
private int m_checked = -1;
private String getSizeString(long size)
/// ListView adapter
private static class SettingsAdapter extends BaseAdapter
{
String arrS[] = { "Kb", "Mb", "Gb" };
private static String TAG = "SettingsAdapter";
private static String MWM_DIR_POSTFIX = "/MapsWithMe/";
long current = 1024;
int i = 0;
for (; i < arrS.length; ++i)
/// @name Different row types.
//@{
private static final int TYPE_HEADER = 0;
private static final int TYPE_ITEM = 1;
private static final int TYPES_COUNT = 2;
//@}
private LayoutInflater m_inflater;
private Activity m_context;
private String m_currPath;
private String m_defPath;
private long m_sizeNeeded;
private int m_listItemHeight;
public SettingsAdapter(Activity context, String currPath, String defPath)
{
final long bound = 1024 * current;
if (size < bound)
break;
else
current = bound;
m_context = context;
m_inflater = m_context.getLayoutInflater();
m_currPath = currPath;
m_defPath = defPath;
m_listItemHeight = (int)Utils.getAttributeDimension(context, android.R.attr.listPreferredItemHeight);
updateList();
}
return String.format("%.1f %s", (double)size / (double)current, arrS[i]);
}
private boolean addStorage(String path, long sizeNeeded)
{
try
private String getSizeString(long size)
{
final File f = new File(path);
if (f.exists() && f.isDirectory() && f.canWrite())
{
StatFs stat = new StatFs(path);
final long size = (long)stat.getAvailableBlocks() * (long)stat.getBlockSize();
Log.i(TAG, "Available size = " + size);
String arrS[] = { "Kb", "Mb", "Gb" };
if (size >= sizeNeeded)
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 static class StorageItem
{
String m_path;
long m_size;
}
private List<StorageItem> m_items = new ArrayList<StorageItem>();
private int m_current = -1;
private boolean isAvailable(int index)
{
assert(index >= 0 && index < m_items.size());
return ((m_current != index) && (m_items.get(index).m_size >= m_sizeNeeded));
}
private void updateList()
{
m_sizeNeeded = getDirSize(m_currPath + MWM_DIR_POSTFIX);
Log.i(TAG, "Needed size for maps: " + m_sizeNeeded);
m_items.clear();
//parseMountFile("/etc/vold.conf", VOLD_MODE, currPath, defPath, sizeNeeded);
//parseMountFile("/etc/vold.fstab", VOLD_MODE, currPath, defPath, sizeNeeded);
parseMountFile("/proc/mounts", MOUNTS_MODE);
m_current = -1;
for (int i = 0; i < m_items.size(); ++i)
if (m_items.get(i).m_path.equals(m_currPath))
{
m_choices.add(path + ": " + getSizeString(size));
m_pathes.add(path);
m_current = i;
break;
}
assert(m_current != -1);
}
private boolean addStorage(String path)
{
try
{
final File f = new File(path);
if (f.exists() && f.isDirectory() && f.canWrite())
{
StatFs stat = new StatFs(path);
final long size = (long)stat.getAvailableBlocks() * (long)stat.getBlockSize();
Log.i(TAG, "Available size = " + size);
StorageItem item = new StorageItem();
item.m_path = path;
item.m_size = size;
m_items.add(item);
return true;
}
else
Log.i(TAG, "File error for storage: " + path);
}
else
Log.i(TAG, "File error for storage: " + path);
}
catch (IllegalArgumentException ex)
{
// Suppress exceptions for unavailable storages.
Log.i(TAG, "StatFs error for storage: " + path);
}
return false;
}
private static int CURRENT_PATH = 1;
private static int DEFAULT_PATH = 2;
/// @return Flag that current path or default path is added successfully.
private int callAddStorage(String path, String currPath, String defPath,
long sizeNeeded, int flag)
{
assert(sizeNeeded > 0);
if (path.equals(currPath))
sizeNeeded = 0;
if (addStorage(path, sizeNeeded))
{
if (path.equals(currPath))
flag |= CURRENT_PATH;
if (path.equals(defPath))
flag |= DEFAULT_PATH;
}
return flag;
}
private static int VOLD_MODE = 1;
private static int MOUNTS_MODE = 2;
private void parseMountFile(String file, int mode,
String currPath, String defPath, long sizeNeeded)
{
BufferedReader reader = null;
int addFlag = 0;
try
{
reader = new BufferedReader(new FileReader(file));
while (true)
catch (IllegalArgumentException ex)
{
String line = reader.readLine();
if (line == null) break;
// Suppress exceptions for unavailable storages.
Log.i(TAG, "StatFs error for storage: " + path);
}
String[] arr = line.split(" ");
if (arr.length >= 3)
return false;
}
private static int CURRENT_PATH = 1;
private static int DEFAULT_PATH = 2;
/// @return Flag that current path or default path is added successfully.
private int callAddStorage(String path, int flag)
{
if (addStorage(path))
{
if (path.equals(m_currPath))
flag |= CURRENT_PATH;
if (path.equals(m_defPath))
flag |= DEFAULT_PATH;
}
return flag;
}
private static int VOLD_MODE = 1;
private static int MOUNTS_MODE = 2;
private void parseMountFile(String file, int mode)
{
BufferedReader reader = null;
int addFlag = 0;
try
{
reader = new BufferedReader(new FileReader(file));
while (true)
{
if (arr[0].charAt(0) == '#')
continue;
String line = reader.readLine();
if (line == null) break;
if (mode == VOLD_MODE)
String[] arr = line.split(" ");
if (arr.length >= 3)
{
Log.i(TAG, "Label = " + arr[1] + "; Path = " + arr[2]);
if (arr[0].charAt(0) == '#')
continue;
if (arr[0].startsWith("dev_mount"))
addFlag = callAddStorage(arr[2], currPath, defPath, sizeNeeded, addFlag);
}
else
{
assert(mode == MOUNTS_MODE);
Log.i(TAG, "Label = " + arr[0] + "; Path = " + arr[1]);
if (mode == VOLD_MODE)
{
Log.i(TAG, "Label = " + arr[1] + "; Path = " + arr[2]);
String prefixes[] = { "tmpfs", "/dev/block/vold", "/dev/fuse" };
for (String s : prefixes)
if (arr[0].startsWith(s))
addFlag = callAddStorage(arr[1], currPath, defPath, sizeNeeded, addFlag);
if (arr[0].startsWith("dev_mount"))
addFlag = callAddStorage(arr[2], addFlag);
}
else
{
assert(mode == MOUNTS_MODE);
Log.i(TAG, "Label = " + arr[0] + "; Path = " + arr[1]);
String prefixes[] = { "tmpfs", "/dev/block/vold", "/dev/fuse" };
for (String s : prefixes)
if (arr[0].startsWith(s))
addFlag = callAddStorage(arr[1], addFlag);
}
}
}
}
}
catch (IOException e)
{
Log.w(TAG, "Can't read file: " + file);
}
finally
{
Utils.closeStream(reader);
catch (IOException e)
{
Log.w(TAG, "Can't read file: " + file);
}
finally
{
Utils.closeStream(reader);
}
// Check that current and default paths are added to the list.
if ((addFlag & CURRENT_PATH) == 0)
addFlag = callAddStorage(m_currPath, addFlag);
if ((addFlag & DEFAULT_PATH) == 0)
addFlag = callAddStorage(m_defPath, addFlag);
assert(addFlag == (CURRENT_PATH | DEFAULT_PATH));
}
// Check that current and default paths are added to the list.
if ((addFlag & CURRENT_PATH) == 0)
addFlag = callAddStorage(currPath, currPath, defPath, sizeNeeded, addFlag);
if ((addFlag & DEFAULT_PATH) == 0)
addFlag = callAddStorage(defPath, currPath, defPath, sizeNeeded, addFlag);
assert(addFlag == (CURRENT_PATH | DEFAULT_PATH));
}
private static int LV_HEADERS_COUNT = 1;
private CheckedTextView getViewByPos(ListView lv, int position)
{
assert(lv.getHeaderViewsCount() == LV_HEADERS_COUNT);
final int child = position - (lv.getFirstVisiblePosition() - LV_HEADERS_COUNT);
if (child < 0 || child >= lv.getChildCount())
/// @name Assume that MapsWithMe folder doesn't have inner folders and symbolic links.
//@{
private long getDirSize(String name)
{
Log.e(TAG, "Unable to get view for desired position: " + position);
return null;
}
return (CheckedTextView) lv.getChildAt(child);
}
File dir = new File(name);
assert(dir.exists());
assert(dir.isDirectory());
private String getFullPath(int position)
{
return m_pathes.get(position - LV_HEADERS_COUNT) + MWM_DIR_POSTFIX;
}
long size = 0;
for (File f : dir.listFiles())
{
assert(f.isFile());
size += f.length();
}
/// @name Assume that MapsWithMe folder doesn't have inner folders and symbolic links.
//@{
private long getDirSize(String name)
{
File dir = new File(name);
assert(dir.exists());
assert(dir.isDirectory());
long size = 0;
for (File f : dir.listFiles())
{
assert(f.isFile());
size += f.length();
return (size + 1024*1024);
}
return (size + 1024*1024);
}
// delete all files (except settings.ini) in directory
private void deleteFiles(File dir)
{
assert(dir.exists());
assert(dir.isDirectory());
for (File file : dir.listFiles())
// delete all files (except settings.ini) in directory
private void deleteFiles(File dir)
{
assert(file.isFile());
assert(dir.exists());
assert(dir.isDirectory());
// skip settings.ini - this file should be always in one place
if (file.getName().equalsIgnoreCase("settings.ini"))
continue;
for (File file : dir.listFiles())
{
assert(file.isFile());
if (!file.delete())
Log.w(TAG, "Can't delete file: " + file.getName());
// skip settings.ini - this file should be always in one place
if (file.getName().equalsIgnoreCase("settings.ini"))
continue;
if (!file.delete())
Log.w(TAG, "Can't delete file: " + file.getName());
}
}
//@}
private static int HEADERS_COUNT = 1;
@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 (m_items != null ? m_items.size() + HEADERS_COUNT : HEADERS_COUNT);
}
@Override
public StorageItem getItem(int position)
{
return (position == 0 ? null : m_items.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 = m_inflater.inflate(android.R.layout.simple_list_item_1, null);
convertView.setMinimumHeight(m_listItemHeight);
}
TextView v = (TextView) convertView;
v.setText(m_context.getString(R.string.maps) + ": " + getSizeString(m_sizeNeeded));
break;
}
case TYPE_ITEM:
{
final int index = getIndexFromPos(position);
final StorageItem item = m_items.get(index);
if (convertView == null)
{
convertView = m_inflater.inflate(android.R.layout.simple_list_item_single_choice, null);
convertView.setMinimumHeight(m_listItemHeight);
}
CheckedTextView v = (CheckedTextView) convertView;
v.setText(item.m_path + ": " + getSizeString(item.m_size));
v.setChecked(index == m_current);
v.setEnabled((index == m_current) || isAvailable(index));
break;
}
}
return convertView;
}
private int getIndexFromPos(int position)
{
final int index = position - HEADERS_COUNT;
assert(index >= 0 && index < m_items.size());
return index;
}
private String getFullPath(int index)
{
assert(index >= 0 && index < m_items.size());
return (m_items.get(index).m_path + MWM_DIR_POSTFIX);
}
private boolean doMoveMaps(String path)
{
if (SettingsActivity.nativeSetStoragePath(path))
{
if (m_current != -1)
deleteFiles(new File(getFullPath(m_current)));
return true;
}
return false;
}
private void doUpdateAfterMove(String path)
{
m_currPath = path;
updateList();
notifyDataSetChanged();
}
private class MoveFilesTask extends AsyncTask<String, Void, Boolean>
{
private ProgressDialog m_dlg;
private String m_resPath;
public MoveFilesTask(Activity context, String path)
{
m_resPath = path;
m_dlg = new ProgressDialog(context);
m_dlg.setMessage(context.getString(R.string.wait_several_minutes));
m_dlg.setProgressStyle(ProgressDialog.STYLE_SPINNER);
m_dlg.setIndeterminate(true);
m_dlg.setCancelable(false);
}
@Override
protected void onPreExecute()
{
m_dlg.show();
}
@Override
protected Boolean doInBackground(String... params)
{
return doMoveMaps(params[0]);
}
@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
{
m_dlg.dismiss();
}
catch (Exception e)
{
}
if (result)
doUpdateAfterMove(m_resPath);
}
}
public void onListItemClick(final int position)
{
final int index = getIndexFromPos(position);
if (isAvailable(index))
{
final String path = getFullPath(index);
File f = new File(path);
if (!f.exists() && !f.mkdirs())
{
Log.e(TAG, "Can't create directory: " + path);
return;
}
new AlertDialog.Builder(m_context)
.setCancelable(false)
.setTitle(R.string.move_maps)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dlg, int which)
{
Log.i(TAG, "Transfer data to storage: " + path);
MoveFilesTask task = new MoveFilesTask(m_context, m_items.get(index).m_path);
task.execute(path);
dlg.dismiss();
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dlg, int which)
{
dlg.dismiss();
}
})
.create()
.show();
}
}
}
//@}
@Override
public void onCreate(Bundle savedInstanceState)
@ -230,94 +479,17 @@ public class SettingsActivity extends ListActivity
final String defPath = Environment.getExternalStorageDirectory().getAbsolutePath();
Log.i(TAG, "Current and Default maps pathes: " + currPath + "; " + defPath);
final long sizeNeeded = getDirSize(currPath + MWM_DIR_POSTFIX);
Log.i(TAG, "Needed size for maps: " + sizeNeeded);
//parseMountFile("/etc/vold.conf", VOLD_MODE, currPath, defPath, sizeNeeded);
//parseMountFile("/etc/vold.fstab", VOLD_MODE, currPath, defPath, sizeNeeded);
parseMountFile("/proc/mounts", MOUNTS_MODE, currPath, defPath, sizeNeeded);
final ListView lv = getListView();
lv.setItemsCanFocus(false);
lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
// add header item with maps size
TextView tv = (TextView) getLayoutInflater().inflate(android.R.layout.simple_list_item_1, lv, false);
tv.setText(getString(R.string.maps) + ": " + getSizeString(sizeNeeded));
lv.addHeaderView(tv, null, false);
setListAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_single_choice, m_choices));
// set current selection by current storage path
for (int i = 0; i < m_pathes.size(); ++i)
if (m_pathes.get(i).equals(currPath))
{
m_checked = i + LV_HEADERS_COUNT;
lv.setItemChecked(m_checked, true);
break;
}
setListAdapter(new SettingsAdapter(this, currPath, defPath));
}
@Override
protected void onListItemClick(final ListView l, View v, final int position, long id)
{
if (position != m_checked)
{
final String path = getFullPath(position);
File f = new File(path);
if (!f.exists() && !f.mkdirs())
{
Log.e(TAG, "Can't create directory: " + path);
return;
}
new AlertDialog.Builder(this)
.setCancelable(false)
.setTitle(R.string.move_maps)
.setMessage(R.string.wait_several_minutes)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dlg, int which)
{
Log.i(TAG, "Transfer data to storage: " + path);
if (nativeSetStoragePath(path))
{
if (m_checked != -1)
deleteFiles(new File(getFullPath(m_checked)));
l.setItemChecked(position, true);
m_checked = position;
}
else if (m_checked != -1)
{
// set check state back to m_checked item
l.setItemChecked(m_checked, true);
}
dlg.dismiss();
}
})
.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dlg, int which)
{
// set check state back to m_checked item
if (m_checked != -1)
l.setItemChecked(m_checked, true);
dlg.dismiss();
}
})
.create()
.show();
}
// Do not process clicks on header items.
if (position != 0)
((SettingsAdapter) getListView().getAdapter()).onListItemClick(position);
}
private native String nativeGetStoragePath();
private native boolean nativeSetStoragePath(String newPath);
private static native boolean nativeSetStoragePath(String newPath);
}

View file

@ -3,6 +3,7 @@ package com.mapswithme.util;
import java.io.Closeable;
import java.io.IOException;
import android.app.Activity;
import android.util.Log;
import android.view.Window;
@ -39,4 +40,14 @@ public class Utils
else
w.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
public static float getAttributeDimension(Activity activity, int attr)
{
android.util.TypedValue value = new android.util.TypedValue();
boolean b = activity.getTheme().resolveAttribute(attr, value, true);
assert(b);
android.util.DisplayMetrics metrics = new android.util.DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
return value.getDimension(metrics);
}
}