Fork me on GitHub

android管理联系人操作

ContentProvider扩展之管理系统联系人

我们都知道ContentProvider是用来共享数据的,然而android本身就提供了大量的ContentProvider,例如联系人信息,系统的多媒体信息等,这些系统的ContentProvider都提供了供其他应用程序访问的Uri,开发者可以通过ContentResolver来调用系统的ContentProvider提供的insert()/update()/delete()/query()方法,从而实现自己的需求。

1、了解系统联系人的结构

(1)android系统对联系人管理提供了很多的Uri,其中用到最多的几个如下:

  ContactsContract.Contacts.CONTENT_URI:管理联系人的Uri

  ContactsContract.CommonDataKinds.Phone.CONTENT_URI:管理联系人电话的Uri

  ContactsContract.CommonDataKinds.Email.CONTENT_URI:管理联系人邮箱的Uri

  ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI:管理联系人地址的Uri

  我们可以根据系统提供的这些Uri去操作系统联系人信息,具体的还有很多CONTENT_URI,可以到ContactsContract.class文件中去查看源码。

(2)表结构

  我们之前说过,ContentProvider是以表的形式来存放这些数据的,系统联系人也是这样,一般存放在data/data/com.android.providers.contacts/databases/contacts2.db中,我们可以通过adb shell命令行的方式来看,也可以将这个contacts2.db文件导出,通过sqlite工具来查看,这里为了方便我们导出来看一下。

找到对应的文件,然后选中点击右上角的导出按钮,将文件导出到系统中的某一个地方

 

将导出的文件用工具打开,我这里使用的是:Navicat Premium

表和视图:

 

我们经常用的到有:contacts、data、raw_contacts三张表:

contacts表:

  _id :表的ID,主要用于其它表通过contacts 表中的ID可以查到相应的数据。
  display_name: 联系人名称
  photo_id:头像的ID,如果没有设置联系人头像,这个字段就为空
  times_contacted:通话记录的次数
  last_time_contacted: 最后的通话时间 
  lookup :是一个持久化的储存 因为用户可能会改名子 但是它改不了lookup

data表:

  raw_contact_id:通过raw_contact_id可以找到 raw_contact表中相对的数据。

  data1 到 data15 这里保存着联系人的信息 联系人名称 联系人电话号码 电子邮件 备注 等等

raw_contacts表:

   version :版本号,用于监听变化
  deleted :删除标志, 0为默认 1 表示这行数据已经删除
  display_name : 联系人名称
  last_time_contacts : 最后联系的时间

 【说明:由于这些表的字段都特别多,截图不全,可以自己试着导出一份看看】

2、代码实现

(1)首先要在AndroidManifest.xml文件中配置对系统联系人的读写权限:

<!-- 添加操作联系人的权限 -->
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.WRITE_CONTACTS" />

(2)布局文件

  首先是列表展示页面:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="50dp"
        android:gravity="center_vertical"
        android:text="我的联系人"
        android:textSize="25dp" />

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:background="@android:color/darker_gray" >

        <TextView
            android:id="@+id/id"
            android:layout_width="0dp"
            android:layout_height="30dp"
            android:layout_weight="1"
            android:text="ID"
            android:gravity="center" />

        <TextView
            android:id="@+id/name"
            android:layout_width="0dp"
            android:layout_height="30dp"
            android:layout_weight="1"
            android:text="姓名"
            android:gravity="center" />

        <TextView
            android:id="@+id/phone"
            android:layout_width="0dp"
            android:layout_height="30dp"
            android:layout_weight="1"
            android:text="手机号码"
            android:gravity="center" />
        
    </LinearLayout>

    <ListView
        android:id="@+id/listview"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:divider="#FF0000"
        android:dividerHeight="1dp"
        android:focusable="true"
        android:minHeight="40dp"
        android:footerDividersEnabled="false" >
    </ListView>

</LinearLayout>
View Code

  列表展示中需要用的listview页面布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal" >
    
    <TextView 
        android:id="@+id/id"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:gravity="center"
        android:layout_height="40dp"
        />
    <TextView 
        android:id="@+id/name"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:gravity="center"
        android:layout_height="40dp"
        />
    <TextView 
        android:id="@+id/phone"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:gravity="center"
        android:layout_height="40dp"
        />

</LinearLayout>
View Code

  添加联系人界面:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="50dp"
        android:layout_gravity="center"
        android:orientation="horizontal" >

        <TextView
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:layout_marginRight="20dp"
            android:gravity="center_vertical|right"
            android:text="姓名:"
            android:textSize="20dp" />

        <EditText
            android:id="@+id/name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="50dp"
        android:layout_gravity="center"
        android:orientation="horizontal" >

        <TextView
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:gravity="center_vertical|right"
            android:layout_marginRight="20dp"
            android:text="手机号码:"
            android:textSize="20dp" />

        <EditText
            android:id="@+id/phone"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
    
     <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="50dp"
        android:layout_gravity="center"
        android:gravity="center"
        android:orientation="horizontal" >
        
         <Button 
             android:id="@+id/save"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="保存"
             android:textSize="20sp"
             />
         
          <Button 
             android:id="@+id/cancel"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="取消"
             android:layout_marginLeft="30dp"
             android:textSize="20sp"
             />
         </LinearLayout>

</LinearLayout>
View Code

  联系人详细界面:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="10dp"
    android:layout_marginLeft="10dp"
    android:layout_marginRight="10dp"
    android:layout_marginTop="10dp"
    android:background="@android:color/darker_gray" >

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="80dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:background="@android:color/white"
        android:gravity="center_vertical|right"
        android:text="姓名:"
        android:textSize="20sp" />

    <TextView
        android:id="@+id/et_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/tv_name"
        android:layout_toRightOf="@+id/tv_name"
        android:background="@android:color/white"
        android:gravity="center_vertical"
        android:text=""
        android:textSize="20sp" />

    <TextView
        android:id="@+id/tv_phone"
        android:layout_width="80dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_name"
        android:layout_marginTop="20dp"
        android:background="@android:color/white"
        android:gravity="center_vertical|right"
        android:text="手机号:"
        android:textSize="20sp" />

    <TextView
        android:id="@+id/et_phone"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/tv_phone"
        android:layout_toRightOf="@+id/tv_phone"
        android:background="@android:color/white"
        android:gravity="center_vertical"
        android:text=""
        android:textSize="20sp" />

    <TextView
        android:id="@+id/tv_email"
        android:layout_width="80dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_phone"
        android:layout_marginTop="20dp"
        android:background="@android:color/white"
        android:gravity="center_vertical|right"
        android:text="Email:"
        android:textSize="20sp" />

    <TextView
        android:id="@+id/et_email"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/tv_email"
        android:layout_toRightOf="@+id/tv_email"
        android:background="@android:color/white"
        android:gravity="center_vertical"
        android:text=""
        android:textSize="20sp" />

    <TextView
        android:id="@+id/tv_address"
        android:layout_width="80dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_email"
        android:layout_marginTop="20dp"
        android:background="@android:color/white"
        android:gravity="center_vertical|right"
        android:text="地址:"
        android:textSize="20sp" />

    <TextView
        android:id="@+id/et_address"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/tv_address"
        android:layout_toRightOf="@+id/tv_address"
        android:background="@android:color/white"
        android:gravity="center_vertical"
        android:text=""
        android:textSize="20sp" />

</RelativeLayout>
View Code

  程序中需要用的menu菜单配置在menu文件中:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <group android:id="@+id/group1">
        <item android:id="@+id/detail" android:title="详情"></item>
        <item android:id="@+id/update" android:title="修改"></item>
        <item android:id="@+id/delete" android:title="删除"></item>
    </group>
    
</menu>

 

(3)Activity代码:

  列表页面以及适配器Adapter

ContactsActivity.java

package com.demo.contentprovider;

import java.util.ArrayList;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.Toast;

import com.demo.adapter.ContactsAdapter;
import com.demo.model.Contact;

/**
 * 手机联系人操作
 * @author yinbenyang
 */
public class ContactsActivity extends Activity {

    private static final int ADD = 0;

    private static final int REQUEST_ADD = 100;

    // 存储联系人的列表
    private ArrayList<Contact> contactList = null;
    // 联系人适配器
    private ContactsAdapter adapter;

    private ListView listview;

    // 弹出式菜单
    public PopupMenu popupmenu = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.contentprovider);
        listview = (ListView) findViewById(R.id.listview);
        init();
        listview.setOnItemLongClickListener(new OnItemLongClickListenerImpl());
        // registerForContextMenu(listview);
    }

    // ----------------------------------------------------选项菜单---------------------------------------------------
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // TODO Auto-generated method stub
        menu.add(0, ADD, 0, "添加");
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // TODO Auto-generated method stub
        switch (item.getItemId()) {
        case ADD:
            Intent intent = new Intent(this, AddContactActivity.class);
            startActivityForResult(intent, REQUEST_ADD);
            break;
        default:
            break;
        }
        return true;
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // TODO Auto-generated method stub
        switch (requestCode) {
        case REQUEST_ADD:
            init();
            break;
        default:
            break;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

    // 初始化,获取联系人
    private void init() {
        ContentResolver resolver = getContentResolver();
        /**
         * ContactsContract.Contacts.CONTENT_URI:手机联系人的Uri:content://com.android
         * .contacts/contacts
         * sort_key_alt:它里面保存的是联系人名字的拼音字母,例如联系人名字是“李明”,则sort_key保存的是“LI李MING明”,
         * 这样如果是按sort_key或sort_key_alt排序的话,就可以将联系人按顺序排列
         */
        Cursor cursor = resolver.query(ContactsContract.Contacts.CONTENT_URI,
                null, null, null, "sort_key_alt");
        contactList = new ArrayList<Contact>();
        while (cursor.moveToNext()) {
            Contact contact = new Contact();
            String phoneNumber = null;
            String name = cursor.getString(cursor
                    .getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
            String id = cursor.getString(cursor
                    .getColumnIndex(ContactsContract.Contacts._ID));
            Cursor phoneCursor = resolver.query(
                    ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,
                    ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = "
                            + id, null, null);
            while (phoneCursor.moveToNext()) {
                phoneNumber = phoneCursor
                        .getString(phoneCursor
                                .getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
            }
            contact.setId(Integer.parseInt(id));
            contact.setName(name);
            contact.setPhone(phoneNumber);
            contactList.add(contact);
        }
        adapter = new ContactsAdapter(this, contactList);
        listview.setAdapter(adapter);
    }

    private class OnItemLongClickListenerImpl implements
            OnItemLongClickListener {

        @Override
        public boolean onItemLongClick(AdapterView<?> parent, View view,
                final int position, long id) {
            final ContentResolver resolver = getContentResolver();
            popupmenu = new PopupMenu(ContactsActivity.this, listview);
            popupmenu.getMenuInflater().inflate(R.menu.my_menu,
                    popupmenu.getMenu());
            popupmenu.setOnMenuItemClickListener(new OnMenuItemClickListener() {
                @Override
                public boolean onMenuItemClick(MenuItem item) {
                    switch (item.getItemId()) {
                    case R.id.detail:
                        String id = contactList.get(position).getId() + "";
                        Intent intent = new Intent(ContactsActivity.this,
                                ContactDetailActivity.class);
                        intent.putExtra("id", id);
                        startActivity(intent);
                        break;
                    case R.id.update:
                        Toast.makeText(ContactsActivity.this, "执行修改操作",
                                Toast.LENGTH_SHORT).show();
                        break;
                    case R.id.delete:
                        new AlertDialog.Builder(ContactsActivity.this)
                                .setTitle("刪除联系人")
                                .setMessage("你确定要删除该联系人吗?")
                                .setPositiveButton("确定", new OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog,
                                            int which) {
                                        int i = resolver
                                                .delete(ContactsContract.RawContacts.CONTENT_URI,
                                                        ContactsContract.Data._ID
                                                                + " = "
                                                                + contactList
                                                                        .get(position)
                                                                        .getId(),
                                                        null);
                                        Toast.makeText(ContactsActivity.this,
                                                i == 1 ? "刪除成功!" : "刪除失敗!",
                                                Toast.LENGTH_SHORT).show();
                                        init();
                                    }
                                })
                                .setNegativeButton("取消", new OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog,
                                            int which) {
                                        dialog.dismiss();
                                    }
                                }).create().show();
                        break;
                    default:
                        break;
                    }
                    // Toast.makeText(ContactsActivity.this,
                    // "你点击了:" + item.getTitle(), Toast.LENGTH_SHORT)
                    // .show();
                    // popupmenu.dismiss();
                    return true;
                }
            });
            popupmenu.show();
            return true;
        }
    }
}

/**
 * <!-- 联系人相关的uri --> content://com.android.contacts/contacts 操作的数据是联系人信息Uri
 * content://com.android.contacts/data/phones 联系人电话Uri
 * content://com.android.contacts/data/emails 联系人Email Uri
 */
View Code

ContactsAdapter.java

package com.demo.adapter;

import java.util.ArrayList;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import com.demo.contentprovider.R;
import com.demo.model.Contact;

/**
 * 联系人的Adapter
 * 
 * @author yinbenyang
 */
public class ContactsAdapter extends BaseAdapter {

    private Context context;
    private ArrayList<Contact> listContact;

    public ContactsAdapter(Context context, ArrayList<Contact> listContact) {
        this.context = context;
        this.listContact = listContact;
    }

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

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

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null) {
            convertView = View
                    .inflate(context, R.layout.listview_contact, null);
            holder = new ViewHolder();
            holder.id = (TextView) convertView.findViewById(R.id.id);
            holder.name = (TextView) convertView.findViewById(R.id.name);
            holder.phone = (TextView) convertView.findViewById(R.id.phone);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        Contact con = listContact.get(position);
        holder.id.setText(con.getId().toString());
        holder.name.setText(con.getName());
        holder.phone.setText(con.getPhone());
        return convertView;
    }

    static class ViewHolder {
        TextView id;
        TextView name;
        TextView phone;
    }

}
View Code

  添加联系人Activity:

AddContactActivity.java

package com.demo.contentprovider;

import android.app.Activity;
import android.content.ContentUris;
import android.content.ContentValues;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

/**
 * 添加联系人信息
 * @author yinbenyang
 *
 */
public class AddContactActivity extends Activity implements OnClickListener{

    private EditText name,phone;
    private Button save,cancel;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.addcontact);
        name = (EditText) findViewById(R.id.name);
        phone = (EditText) findViewById(R.id.phone);
        save = (Button) findViewById(R.id.save);
        cancel = (Button) findViewById(R.id.cancel);
        save.setOnClickListener(this);
        cancel.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
        switch (v.getId()) {
        case R.id.save:
            String _name = name.getText().toString().replaceAll(" ", "");
            String _phone = phone.getText().toString().replaceAll(" ","");
            ContentValues values = new ContentValues();
             //首先向RawContacts.CONTENT_URI执行一个空值插入,目的是获取系统返回的rawContactId
            Uri rawContactUri =getContentResolver().insert(RawContacts.CONTENT_URI, values);
            long rawContactId = ContentUris.parseId(rawContactUri);
            //往data表入姓名数据
            values.clear();
            values.put(Data.RAW_CONTACT_ID, rawContactId);
            //设置内容类型
            values.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
            //设置内容名字
            values.put(StructuredName.GIVEN_NAME, _name);
            getContentResolver().insert(
                    ContactsContract.Data.CONTENT_URI, values);
            
          //往data表入电话数据
            values.clear();
            values.put(Data.RAW_CONTACT_ID, rawContactId);
            values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
            //设置电话号码
            values.put(Phone.NUMBER, _phone);
            //设置电话类型
            values.put(Phone.TYPE, Phone.TYPE_MOBILE);
            getContentResolver().insert(
                    ContactsContract.Data.CONTENT_URI, values);
            this.finish();
            break;
        case R.id.cancel:
            this.finish();
            break;
        default:
            break;
        }
    }
}
View Code

  联系人详细页面:

ContactDetailActivity.java

package com.demo.contentprovider;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.TextView;

/**
 * 联系人详细信息
 * 
 * @author yinbenyang
 *
 */
public class ContactDetailActivity extends Activity {

    private TextView et_name, et_phone, et_email, et_address;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.contactdetail);
        et_name = (TextView) findViewById(R.id.et_name);
        et_phone = (TextView) findViewById(R.id.et_phone);
        et_email = (TextView) findViewById(R.id.et_email);
        et_address = (TextView) findViewById(R.id.et_address);
        Intent intent = getIntent();
        String id = intent.getStringExtra("id");
        ContentResolver resolver = getContentResolver();

        // 查找姓名
        Cursor nameCursor = resolver.query(
                ContactsContract.Contacts.CONTENT_URI,
                null,
                ContactsContract.Contacts._ID + " = " + id,
                null, null);
        while(nameCursor.moveToNext()){
            et_name.setText(nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)));
        }
        // 查找手机号
        Cursor phoneCursor = resolver.query(
                ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,
                ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + id,
                null, null);
        while (phoneCursor.moveToNext()) {
            et_phone.setText(phoneCursor.getString(phoneCursor
                    .getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
        }
        // 查找邮箱
        Cursor emailCursor = resolver.query(
                ContactsContract.CommonDataKinds.Email.CONTENT_URI, null,
                ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = " + id,
                null, null);
        while (emailCursor.moveToNext()) {
            et_email.setText(emailCursor.getString(emailCursor
                    .getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA)));
        }

        // 查找地址
        Cursor addressCursor = resolver.query(
                ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI,
                null,
                ContactsContract.CommonDataKinds.StructuredPostal.CONTACT_ID
                        + " = " + id, null, null);
        while (addressCursor.moveToNext()) {
            et_address
                    .setText(addressCursor.getString(addressCursor
                            .getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.DATA)));
        }
    }
}
View Code

最后效果图如下。可以实现联系人的添加,删除和修改功能:

 

posted on 2015-06-08 09:59  骑着乌龟漫步  阅读(1276)  评论(6编辑  收藏  举报

导航