乐者为王

Do one thing, and do it well.

如何创建Android应用启动界面

1、制作一张启动图片splash.jpg,放在res/drawable文件夹中。

2、新建布局文件splash.xml:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <ImageView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:scaleType="fitXY"
        android:src="@drawable/splash" />
</LinearLayout>

3、建立SplashActivity,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.MotionEvent;

public class SplashActivity extends Activity {
    private boolean active = true;
    private int splashTime = 5000;    // time to display the splash screen in ms

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.splash);

        new Thread() {
            @Override
            public void run() {
                try {
                    int waited = 0;
                    while (active && (waited < splashTime)) {
                        sleep(100);
                        if (active) {
                            waited += 100;
                        }
                    }
                } catch(InterruptedException e) {
                } finally {
                    finish();

                    // Run next activity
                    Intent intent = new Intent(SplashActivity.this, MainActivity.class);
                    startActivity(intent);
                    //stop();
                }
            }
        }.start();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            active = false;
        }
        return true;
    }
}

4、修改AndroidManifest.xml文件,将启动界面Activity改为默认启动,并且设置标题栏不可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<application android:icon="@drawable/icon" android:label="@string/app_name">
    <activity android:name=".SplashActivity"
              android:theme="@android:style/Theme.NoTitleBar">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

    <activity android:name=".MainActivity"
              android:label="@string/app_name">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
</application>

导入UC Browser书签到Firefox中

UC Browser保存的书签后缀名是aucf,不能直接导入到Firefox中,必须要把aucf格式转成Firefox支持的格式才行。

先把UC Browser和Firefox的书签格式都分析了一下。Firefox的格式非常简单,就是个HTML文件。UC的是二进制格式,文件以字符串android开头,接着是n个byte的信息,没有分析出来是啥。然后4个byte是书签的数目,再接着的n个byte没去分析,再下来就是每个书签的信息了。书签信息分两部分:名称和地址。都是第1个byte说明后面字符串的长度,字符串以00结尾。

1
2
3
4
5
6
7
8
android    <- 文件头
xxx...     <- n个byte
xxxx       <- 保存的书签数目
xxx...     <- n个byte
xx         <- 书签名称长度
xxx...     <- 书签名称
xx         <- 书签地址长度
xxx...     <- 书签地址

用文本文件打开时可以查看英文字符。书签的地址通常由英文字母组成,所以我们不需要再去分析整个aucf文件的格式,只要把所有书签地址提取出来就行了。下面就是完整的Ruby代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# aucf2ff.rb
if ARGV.length != 1 then
  puts "USAGE: ruby aucf2ff.rb ucfavorite"
  exit
end

ucfavorite = ARGV[0]
if !FileTest.exist?(ucfavorite) then
  puts "File #{ucfavorite} was not found"
  exit
end

ff = File.new(ucfavorite.gsub(".aucf", "_ff.html"), "w")
ff.puts '<!DOCTYPE NETSCAPE-Bookmark-file-1>'
ff.puts '<!-- This is an automatically generated file.'
ff.puts '     It will be read and overwritten.'
ff.puts '     DO NOT EDIT! -->'
ff.puts '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">'
ff.puts '<TITLE>Bookmarks</TITLE>'
ff.puts '<H1>Bookmarks Menu</H1>'
ff.puts
ff.puts '<DL><p>'
ff.puts '    <DT><H3>UC Bookmarks</H3>'
ff.puts '    <DL><p>'

buffer = IO.read(ucfavorite)
index = buffer.index("http://")
favorites = buffer.slice(index, buffer.length - index)
favorites.split("http://").each do |fav|
  uri = ''
  fav.each_byte do |c|
    break if c == 00
    uri += c.chr
  end
  if (!uri.empty?) then
    ff.puts '<DT><A HREF="http://' + uri + '" LAST_CHARSET="utf-8">' + uri + '</A>'
  end
end

ff.puts '    </DL><p>'
ff.puts '</DL>'
ff.close

通过上面的程序就可以将UC Browser的书签转成Firefox格式,然后在Firefox中导入就可以了。

反编译Android APK文件

2017/9/13更新

classes.dex是Java源代码编译后生成的字节码文件。由于Android使用的Dalvik虚拟机与标准的Java虚拟机是不兼容的,因此,dex文件与class文件相比,不论是文件结构还是opcode都不一样。

目前市面上有多种反编译dex的工具,下面我们就来把它们逐个介绍下。

dexdump

dexdump是Android软件开发套件提供的反编译工具。用法是:首先启动Android模拟器,把要反编译的dex文件用adb push命令推送到其中,然后通过adb shell登录,找到该dex文件,执行dexdump demo.dex。总的来说dexdump功能比较弱,且用起来麻烦,反编译的结果的可读性也很差。

Dedexer

另一个反编译工具是Dedexer,反编译的效果比较好。它可以读取dex格式的文件,生成一种类似于汇编语言的输出。与dexdump相比它至少有3个优点:

1. 不需要在Android模拟器中运行; 2. 反编译后的文件目录结构和源代码的目录结构相近,每个class文件对应一个ddx文件,不像dexdump那样把所有的结果都放在一起; 3. 可以作为反编译引擎。

它的使用的方法如下:

java -jar ddx.jar -o -D -r -d src classes.dex

AXMLPrinter2

APK中的资源通常是压缩过的,用文本工具看都是乱码,不过我们可以用AXMLPrinter2将其转换成可读的XML文件。具体命令为:

java -jar AXMLPrinter2.jar demo.xml output.xml

Apktool

目前最好的反编译工具是Apktool。可以帮助我们把APK文件反编译,输出smali格式的代码、图片和资源等文件,还可以在修改后重新打包。将下载下来的apktool和apktool-install-windows解压到同一目录,可以看到有三个文件:aapt.exe,apktool.bat和apktool.jar。以下是它的使用方法:

1
2
java -jar apktool.jar d demo.apk  # Decoding
java -jar apktool.jar b demo  # Building

dex2jar + JD-GUI

dex2jar是一个能操作Android的dex文件和Java的class的工具集合。它包含有将dex文件转换成jar文件或smali代码的工具:

1
2
d2j-dex2jar.bat demo.apk  # 将文件中的classes.dex转换成jar文件
d2j-dex2smali.bat demo.apk  # 将文件中的classes.dex转换成smali代码

生成的jar文件可以用JD-GUI工具直接打开查看。

JD-GUI从2015年9月开始就停止了维护,对此有担忧的同学可以使用jadx作为JD-GUI的替代品。实际上JD-GUI/jadx是可以直接打开APK文件的。

smali

smali可以反编译dex文件,也可以把你修改过的代码重新编译成dex:

1
2
java -jar baksmali.jar classes.dex -o classes
java -jar smali.jar classes -o classes.dex

将Nokia 6670的短信导入HTC Hero G3

这事本该N久前就该干了(09年末把手机从Nokia 6670换到HTC Hero G3),却一直拖着没有做,今天终于把这事作了一个了结。

Nokia下导出的短信是如Messages_Nokia 6670_20091215.xml这样的一个XML文件,文件内容的编码是unicode,格式就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="unicode"?>
<SMS PhoneModel="Nokia 6670">
  <Message DateTime="2011-06-21 10:10:10">
    <Sender>路人甲</Sender>
    <PhoneNumber>+8613988888888</PhoneNumber>
    <Location>Inbox</Location>
    <Text>我是路人甲</Text>
  </Message>

  <Message DateTime="2011-06-21 11:11:11">
    <Sender>路人乙</Sender>
    <PhoneNumber>+8613966666666</PhoneNumber>
    <Location>Outbox</Location>
    <Text>我是路人乙</Text>
  </Message>
</SMS>

本来是想写个Android应用来直接导入这些备份短信的,不过在实现的过程中发现了SMS Backup&Restore应用导出的备份文件和Nokia的备份文件类似,产生了做个将Nokia备份文件转换成SMS Backup&Restore备份文件的念头,又在网上发现了nicholashan的将Nokia的短信导入Hero(可能适用所有Android机型),坚定了我的这个想法。

分析了一下SMS Backup&Restore备份文件的格式,文件内容的编码是utf-8的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<smses count="3">
  <sms protocol="0" address="10658658" date="1308629532000"
    type="1" subject="null" body="我是内容" toa="null"
    sc_toa="null" service_center="null" read="1"
    status="-1" locked="0" />
  <sms protocol="0" address="13566666666" date="1308633733000"
    type="2" subject="null" body="我也是内容" toa="null"
    sc_toa="null" service_center="null" read="1"
    status="-1" locked="0" />
  <sms protocol="0" address="+8613588888888" date="1308641377000"
    type="1" subject="null" body="我还是内容" toa="null"
    sc_toa="null" service_center="null" read="1"
    status="-1" locked="0" />
</smses>

与转换相关的就address、date、type和body四个属性。address是发信人的号码;date是短信发送日期的timestamp。这点nicholashan的确说错了,后面不需要加什么0的(实际上nicholashan自己也怀疑自己的说法);type等于1表示别人发给你的短信,如果是2的话就是你发给其他人的短信;body就是短信内容了。

将Nokia中的时间转换到SMS Backup&Restore的时间可以使用下面的代码:

1
2
3
4
5
6
7
8
DateFormat dt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
long value = 1355296332000L;    // 2012-12-12 12:12:12
Element e = (Element)iter.next();
Attribute attr = e.attribute("DateTime");
try {
    value = dt.parse(attr.getValue()).getTime();
} catch (ParseException ex) {
}

完整的代码如下(使用dom4j实现XML解析):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.List;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;

public class Main {

    public static void main(String[] args) {
        try {
            SAXReader sr = new SAXReader();
            Document doc = sr.read(new File(args[0]));

            generateDocument(doc);
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void generateDocument(Document doc) {
        List list = doc.selectNodes("//Message");

        Document document = DocumentHelper.createDocument();
        Element smses = document.addElement("smses");
        smses.addAttribute("count", String.valueOf(list.size()));

        DateFormat dt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        long value = 1355296332000L;    // 2012-12-12 12:12:12
        Iterator iter = list.iterator();
        while (iter.hasNext()) {
            Element e = (Element)iter.next();
            Attribute attr = e.attribute("DateTime");
            try {
                value = dt.parse(attr.getValue()).getTime();
            } catch (ParseException e) {
                System.out.println(e.getMessage());
            }
            Node phone = e.selectSingleNode("PhoneNumber");
            Node location = e.selectSingleNode("Location");
            Node text = e.selectSingleNode("Text");

            Element sms = smses.addElement("sms");
            sms.addAttribute("protocol", "0");
            sms.addAttribute("address", phone.getText());
            sms.addAttribute("date", String.valueOf(value));
            if ("Inbox".equals(location.getText())) {
                sms.addAttribute("type", "1");
            } else if ("Outbox".equals(location.getText())) {
                sms.addAttribute("type", "2");
            }
            sms.addAttribute("subject", "null");
            sms.addAttribute("body", text.getText());
            sms.addAttribute("toa", "null");
            sms.addAttribute("sc_toa", "null");
            sms.addAttribute("service_center", "null");
            sms.addAttribute("read", "1");
            sms.addAttribute("status", "-1");
            sms.addAttribute("locked", "0");
        }

        File f = new File("output.xml");
        XMLWriter output = new XMLWriter(new FileWriter(f));
        output.write(document);
        output.close();
    }
}

如何获得已安装应用的安装时间和占用空间

可以通过ApplicationInfo类中sourceDir取得应用的文件路径,再使用File类读取文件的相关属性实现。不过这可能导致:

  1. 无法获取原始的创建时间,可能很早就被创建了,之后被替换了;
  2. 如果这个应用在一个私有的位置,比如app-private目录(使用Market付费购买的应用在这个位置),没有ROOT权限的手机会导致读取失败。
1
2
3
4
5
6
7
8
List<PackageInfo> pkgs = getPackageManager().getInstalledPackages(0);
for (int i = 0; i < pkgs.size(); i++) {
    PackageInfo pkg = pkgs.get(i);

    File file = new File(pkg.applicationInfo.sourceDir);
    System.out.println("file size: " + file.length());
    System.out.println("file last modified: " + file.lastModified());
}

不过File类中文件大小和文件最后修改时间的值是long型,不是用户友好的,在显示前需要格式化一下:

1
2
3
4
5
6
7
String size = Formatter.formatFileSize(context, file.length())

Date date = new Date(file.lastModified())
String lastModified = new SimpleDateFormat("yyyy-MM-dd").format(date)

System.out.println("file size: " + size);
System.out.println("file last modified: " + lastModified);

从Android 2.3 API Level为9开始,ApplicationInfo类新增了firstInstallTime和lastUpdateTime两个字段,可以直接获取应用的创建和最后修改时间,即使是付费软件也能正常获取。

给ListView添加卸载功能的Button

修改main.xml,添加Button控件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1.0">
        <ListView
            android:id="@+id/list_view"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">
        <Button
            android:id="@+id/uninstall_button"
            android:text="Uninstall Selected Apps"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>

然后在MainActivity类中添加卸载应用的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private UninstallReceiver mUninstallReceiver;

@Override
public void onCreate(Bundle savedInstanceState) {
    Button btn = (Button)findViewById(R.id.uninstall_button);
    btn.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            for (int i = 0; i < mListData.size(); i++) {
                HashMap<String, Object> map = mListData.get(i);
                boolean isSelected = (Boolean)map.get("app_select");
                if (isSelected) {
                    Uri packageUri = Uri.parse("package:" + map.get("app_package"));
                    Intent intent = new Intent(Intent.ACTION_DELETE, packageUri);
                    startActivity(intent);
                }
            }
        }
    });

    // 监听卸载广播
    mUninstallReceiver = new UninstallReceiver();
    IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
    filter.addDataScheme("package");
    registerReceiver(mUninstallReceiver, filter);
}

// 有时选择移除的程序在确认时被取消了,因此要用BroadcastReceiver监听应用是否被真正地卸载
private class UninstallReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context ctx, Intent intent) {
        Log.d("UninstallReceiver.onReceive()", intent.getDataString());

        for (int i = 0; i < mListData.size(); i++) {
            HashMap<String, Object> map = mListData.get(i);
            String packageUri = "package:" + map.get("app_package");
            if (packageUri.equals(intent.getDataString())) {
                mListData.remove(map);
            }
        }
        mHandler.sendEmptyMessage(0);
    }
}

@Override
public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mUninstallReceiver);
}

向ListView中添加CheckBox

修改list_item.xml文件,添加CheckBox控件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/app_icon"
        android:layout_gravity="center_vertical"
        android:layout_width="32.0dip"
        android:layout_height="32.0dip"
        android:layout_marginLeft="3.0dip"
        android:layout_marginRight="3.0dip" />
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1.0">
        <TextView
            android:id="@+id/app_title"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="16.0dip"
            android:textStyle="bold" />
        <TextView
            android:id="@+id/app_package"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="13.0dip" />
    </LinearLayout>
    <CheckBox
        android:id="@+id/app_select"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:focusable="false"
        android:focusableInTouchMode="false"
        android:clickable="false"
        android:checkMark="?android:attr/listChoiceIndicatorMultiple" />
</LinearLayout>

这三行代码很重要,如果不加会出现一些奇怪的错误。

1
2
3
android:focusable="false"
android:focusableInTouchMode="false"
android:clickable="false"

加入CheckBox后,在程序运行过程中会发现整个ListView无法响应onItemClick、onItemLongClick或onCreateContextMenu事件。原因在于CheckBox是拥有焦点的,它的优先级比ListItem的焦点优先级更高,于是ListItem的点击事件被屏蔽了。解决方法是让CheckBox不能获得焦点。android:focusable="false"就是做这个的。

修改整个SimpleIconAdapter类为以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class SimpleIconAdapter extends BaseAdapter {
    private LayoutInflater mInflater;
    private List<? extends Map<String, ?>> mData;

    public SimpleIconAdapter(Context context, List<? extends Map<String, ?>> data) {
        mInflater = LayoutInflater.from(context);
        mData = data;
    }

    @Override
    public int getCount() {
        return mData.size();
    }

    @Override
    public Object getItem(int position) {
        return mData.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view;
        if (convertView != null) {
            view = convertView;
        } else {
            view = mInflater.inflate(R.layout.list_item, parent, false);
        }

        Map map = (Map)getItem(position);
        TextView tv = (TextView)view.findViewById(R.id.app_title);
        tv.setText((String)map.get("app_title"));

        tv = (TextView)view.findViewById(R.id.app_package);
        tv.setText((String)map.get("app_package"));

        ImageView iv = (ImageView)view.findViewById(R.id.app_icon);
        iv.setImageDrawable((Drawable)map.get("app_icon"));

        CheckBox cb = (CheckBox)view.findViewById(R.id.app_select);
        if ((Boolean)map.get("app_select") ==  null) {
            map.put("app_select", false);
        }
        cb.setChecked((Boolean)map.get("app_select"));

        return view;
    }
}

再就是修改MainActivity类的onCreate()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    mListView = (ListView)findViewById(R.id.list_view);
    mListData = new ArrayList<HashMap<String, Object>>();
    mAdapter = new SimpleIconAdapter(this, mListData);
    mListView.setAdapter(mAdapter);
    mListView.setItemsCanFocus(false);
    mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
    mListView.setOnItemClickListener(new OnItemClickListener() {
        @SuppressWarnings({ "unchecked", "rawtypes" })
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            // 这里如此处理是因为当选中CheckBox,滚动ListView的时候,会出现一些
            // CheckBox选择错位的现象,所以在选择CheckBox时,记下其状态,然后在
            // getView方法中进行设置。
            //
            // 原因:
            // ListView中的getChildCount()并不总是等于Adapter中的数据行数。当手
            // 机一屏显示不了所有数据时(需要翻页),getChildCount()就等于一屏
            // 所显示的行数(一般为10),小于Adapter中的数据行数。而ListView的
            // getCount()与Adapter中的数据行数相等。
            // 当光标下移到屏幕最底部,新显示出来的View,在调用Adapter的getView
            // 方法中,会判断convertView为null,而再有新的View显示就会发现
            // convertView不为空,所以新显示的View其实使用了之前某个View的对象。
            // 这就造成了状态可能混乱。比如第一行的CheckBox点选时,第11行的也同
            // 时会被点选。
            CheckBox cb = (CheckBox)view.findViewById(R.id.app_select);
            cb.toggle();
            ((Map)mListData.get(position)).put("app_select", cb.isChecked());
        }
    });

    mProgressDialog = ProgressDialog.show(this, "Wait", "loading...");
    new Thread() {
        @Override
        public void run() {
            mListData.addAll(getInstalledApps(false));
            mHandler.sendEmptyMessage(0);
        }
    }.start();
}

实现带Icon的ListView(改进版)

带Icon的ListView工作起来还不错,但是如果手机里安装的软件比较多的话就有问题了。在程序启动时会黑屏一段时间,就好像程序挂起了一样。所以这时需要显示一个对话框,告诉用户正在加载数据,最好还能准确告诉用户加载的进度。

显示对话框用ProgressDialog,它有两种样式:HORIZONTAL和SPINNER。这里暂时选择SPINNER,等以后有机会再改成HORIZONTAL。

我们在加载数据前显示ProgressDialog,在之后关闭对话框。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
ProgressDialog pd = ProgressDialog.show(this, "Wait", "loading...");

ListView lv = (ListView)findViewById(R.id.list_view);
adapter = new SimpleIconAdapter(this,
        getInstalledApps(false),
        R.layout.list_item,
        new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
        new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
lv.setAdapter(adapter);

pd.dismiss();

但结果ProgressDialog没有显示出来,因为加载数据的代码也执行在UI线程中,所以对话框的显示被阻塞了。怎么办呢?可以试着创建一个新线程,然后把加载数据这个耗时的操作放到这个非UI线程中去执行,这样不就可以了么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ProgressDialog pd = ProgressDialog.show(this, "Wait", "loading...");

new Thread() {
    @Override
    public void run() {
        ListView lv = (ListView)findViewById(R.id.list_view);
        adapter = new SimpleIconAdapter(Main.this,
                getInstalledApps(false),
                R.layout.list_item,
                new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
                new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
        lv.setAdapter(adapter);
    }
}.start();

pd.dismiss();

这下更惨了,直接报RuntimeException,连界面都打不开了。

深入研究一下Android的线程模型,看看Android到底是怎么处理UI线程和其它线程的交互的,为什么上面的代码会报异常?

When an application is launched, the system creates a thread called "main" for the application. The main thread, also called the UI thread, is very important because it is in charge of dispatching the events to the appropriate widgets, including drawing events. It is also the thread where your application interacts with running components of the Android UI toolkit.

For instance, if you touch the a button on screen, the UI thread dispatches the touch event to the widget, which in turn sets its pressed state and posts an invalidate request to the event queue. The UI thread dequeues the request and notifies the widget to redraw itself.

This single-thread model can yield poor performance unless your application is implemented properly. Specifically, if everything is happening in a single thread, performing long operations such as network access or database queries on the UI thread will block the whole user interface. No event can be dispatched, including drawing events, while the long operation is underway. From the user's perspective, the application appears hung. Even worse, if the UI thread is blocked for more than a few seconds (about 5 seconds currently) the user is presented with the infamous "application not responding" (ANR) dialog.

看了上面这段话可以知道为啥程序一打开就黑屏,像挂掉了一样。

那为什么用新建线程来执行耗时操作的代码也有问题呢?这是因为它虽然没有阻塞UI线程,但它违背了单线程模型,Android的UI操作并不是线程安全的,并且这些操作必须在UI线程中执行。在这段代码片段中,在另一个线程中执行数据加载绑定,这会引起一些古怪的问题。

Andriod提供了几种在其它线程中访问UI线程的方法:

上面的任何一个类或方法都可以修复我们前面代码中遇到的问题,很不幸的是这些类或方法同样会使你的代码变的很复杂很难理解。而且当你需要实现一些很复杂的操作并需要频繁地更新UI时会变得更糟糕。为了解决这个问题,Android 1.5提供了一个工具类AsyncTask,它使创建需要与用户界面交互的长时间运行的任务变得更简单。在Android 1.0和1.1中具有与AsyncTask相同功能的类UserTask,它提供了完全一样的API,你需要做的只是把它的代码拷贝的你的程序中。

其实这些方案背后采用的都是Handler。所以这里仍然使用旧有的,复杂的Handler来修复上述问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ListView;

public class MainActivity extends Activity {
    private static final String APP_ICON = "app_icon";
    private static final String APP_TITLE = "app_title";
    private static final String APP_PACKAGE = "app_package";

    private SimpleIconAdapter adapter;
    private List<HashMap<String, Object>> listData;
    private ProgressDialog pd;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ListView lv = (ListView)findViewById(R.id.list_view);
        listData = new ArrayList<HashMap<String, Object>>();
        adapter = new SimpleIconAdapter(this,
                listData,
                R.layout.list_item,
                new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
                new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
        lv.setAdapter(adapter);

        pd = ProgressDialog.show(this, "Wait", "loading...");

        new Thread() {
            @Override
            public void run() {
                // 耗时操作,加载数据
                listData.addAll(getInstalledApps(false));
                handler.sendEmptyMessage(0);
            }
        }.start();
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // 通知UI更新。必须要放在这里
            adapter.notifyDataSetChanged();

            pd.dismiss();
        }
    };

no such file to load -- xxx (MissingSourceFile) on Heroku

终于解决掉了困扰我多日的一个问题。

部署在Heroku上的项目,在控制器中require 'calc'时总是出现:

1
no such file to load -- calc (MissingSourceFile)

本地机器上运行的好好的,在Heroku上就出现Application error,百思不得其解,网上也没有找到类似问题的解决方法。

咨询Heroku的技术支持,几封邮件交流下来对方也没有办法(感觉Heroku的技术支持不咋的)。

今天把问题解决掉后再回过头来看,发现这其实是个非常简单的问题,就是Linux和Windows对待文件名大小写的差异。因为Linux是区分大小写的,所以用require 'calc'引入Calc.rb时就会失败,而Windows则可以成功。将文件名Calc.rb改成calc.rb再push到Heroku就把问题解决了。

就这么个大写字母竟然让我思考了几天,还真是郁闷啊!

实现带Icon的ListView

main.xml不需要修改,还是用原来的那个:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <ListView
        android:id="@+id/list_view"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

在list_item.xml中添加ImageView控件,设置高度和宽度分别为32dip,这是因为有些应用Icon尺寸比较大的缘故,如果不设置的话有些行就会撑大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/app_icon"
        android:layout_gravity="center_vertical"
        android:layout_width="32.0dip"
        android:layout_height="32.0dip"
        android:layout_marginLeft="3.0dip"
        android:layout_marginRight="3.0dip" />
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1.0">
        <TextView
            android:id="@+id/app_title"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="16.0dip"
            android:textStyle="bold" />
        <TextView
            android:id="@+id/app_package"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="13.0dip" />
    </LinearLayout>
</LinearLayout>

新建SimpleIconAdapter类,继承SimpleAdapter适配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.List;
import java.util.Map;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SimpleAdapter;

public class SimpleIconAdapter extends SimpleAdapter {

    public SimpleIconAdapter(Context context, List<? extends Map<String, ?>> data,
            int resource, String[] from, int[] to) {
        super(context, data, resource, from, to);
    }

    @SuppressWarnings("rawtypes")
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = super.getView(position, convertView, parent);
        ImageView iv = (ImageView)view.findViewById(R.id.app_icon);
        iv.setImageDrawable((Drawable)((Map)getItem(position)).get("app_icon"));
        return view;
    }
}

修改MainActivity文件,更换绑定的适配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.app.Activity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleAdapter;

public class MainActivity extends Activity {
    private static final String APP_ICON = "app_icon";
    private static final String APP_TITLE = "app_title";
    private static final String APP_PACKAGE = "app_package";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ListView listView = (ListView)findViewById(R.id.list_view);
        SimpleAdapter adapter = new SimpleIconAdapter(this,
                getInstalledApps(false),
                R.layout.list_item,
                new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
                new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
        listView.setAdapter(adapter);
    }

    private List<HashMap<String, Object>> getInstalledApps(boolean getSysPackages) {
        List<HashMap<String, Object>> listItem = new ArrayList<HashMap<String, Object>>();

        List<PackageInfo> pkgs = getPackageManager().getInstalledPackages(0);
        for (int i = 0; i< pkgs.size(); i++) {
            PackageInfo pkg = pkgs.get(i);
            if (!getSysPackages && (pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) > 0) {
                continue;
            }
            Drawable icon = pkg.applicationInfo.loadIcon(getPackageManager());
            String label = pkg.applicationInfo.loadLabel(getPackageManager()).toString();
            String version = pkg.versionName;
            String pkgName = pkg.packageName;

            HashMap<String, Object> map = new HashMap<String, Object>();
            map.put(APP_ICON, icon);
            map.put(APP_TITLE, label + " " + version);
            map.put(APP_PACKAGE, pkgName);
            listItem.add(map);
        }
        return listItem;
    }
}