1. 程式人生 > >Android6.0原始碼分析之藍芽顯示接收到的檔案

Android6.0原始碼分析之藍芽顯示接收到的檔案

在藍芽介面有個menu:顯示接收到的檔案。本文分析顯示接收到的檔案

chapter one---顯示接收到的檔案

/android/packages/apps/Settings/src/com/android/settings/bluetooth/資料夾下的BluetoothSettings.java開始分析

case MENU_ID_SHOW_RECEIVED:
                MetricsLogger.action(getActivity(), MetricsLogger.ACTION_BLUETOOTH_FILES);
                Intent intent = new Intent(BTOPP_ACTION_OPEN_RECEIVED_FILES);
                getActivity().sendBroadcast(intent);
                return true;

當點選顯示接收到的檔案menu時會發送廣播,傳送的廣播為
private static final String BTOPP_ACTION_OPEN_RECEIVED_FILES =
            "android.btopp.intent.action.OPEN_RECEIVED_FILES";
既然有傳送廣播,就要看哪個地方接收到廣播並進行了處理

通過程式碼搜尋定位到/android/packages/apps/Bluetooth/資料夾下的Androidmanifest.xml檔案中進行了定義

可以看到實在opp資料夾下的BluetoothOppReceiver中進行處理的

在Constants中定義了全域性變數

 /** the intent that gets sent from the Settings app to show the received files */
   public static final String ACTION_OPEN_RECEIVED_FILES = "android.btopp.intent.action.OPEN_RECEIVED_FILES";

在BluetoothOppReceiver中當檢測到該action時會進行如下處理

 else if (action.equals(Constants.ACTION_OPEN_RECEIVED_FILES)) {
            if (V) Log.v(TAG, "Received ACTION_OPEN_RECEIVED_FILES.");

            Intent in = new Intent(context, BluetoothOppTransferHistory.class);
            in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            in.putExtra("direction", BluetoothShare.DIRECTION_INBOUND);
            in.putExtra(Constants.EXTRA_SHOW_ALL_FILES, true);
            context.startActivity(in);

跳轉到BluetoothTransferHistory,在跳轉時對intent做了一些處理,首先是設定了一些flags,說明一下

Intent.FLAG_ACTIVITY_CLEAR_TOP:如果在棧中有該例項,就會去重用該例項,並且會清除掉該例項上方的所有activity,簡單舉個例子,如果在棧1中存在有三個例項,Acivity1,Activity2,Activity3。可以看到處於棧頂的是Activity3,也就是目前顯示的是視窗3,如果從視窗3跳轉到視窗2,則會銷燬Activity3,並且重用Activity2,也就是說目前棧中Activity存在情況如下Activity1,Activity2。

Intent.FLAG_ACTIVITY_NEW_TASK:如果在activity節點下存在taskAffinity屬性,首先看該屬性值有沒有進行設定

            android:taskAffinity="">
如果設定了該屬性值,就會去查詢taskAffinity對應的棧,如果棧不存在,則會新建該棧 並將activity存入,如果棧存在,則直接入棧

如果沒有設定該屬性或者該屬性值預設為空,則直接壓入當前棧。在程式中未對BluetoothTransferHistory的該屬性進行設定。

接下來對BluetoothTransferHistory.java分析,該類位於\android\packages\apps\Bluetooth\src\com\android\bluetooth\opp


設計思路:對於顯示藍芽接受到的檔案是利用ContentProvider來訪問uri獲取到已接受到的檔案並顯示出來。

佈局檔案為bluetooth_transfers_page.xml

setContentView(R.layout.bluetooth_transfers_page);

該佈局檔案所用到的節點有些特別,分析一下

<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <ListView
        android:id="@+id/list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"/>
    <ViewStub
        android:id="@+id/empty"
        android:layout="@layout/no_transfers"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"/>
</merge>

首先最外層佈局為merge,接下來是兩個子view:ListView和ViewStub。ListView很常見,但很少用到merge和ViewStub佈局控制元件

關於這些的介紹想了解的可以看相關連結,在這裡不再多說

其中merge是預設的垂直的線性佈局,也就是說該佈局檔案中顯示一個listview列表,然後是一個動態佈局的ViewStub,所引用的layout檔案為no_transfers

no_transfers.xml

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:text="@string/no_transfers"
    android:gravity="center"
    android:textStyle="bold"
    />

顯示一個textview,
 <string name="no_transfers" msgid="3482965619151865672">"沒有傳輸歷史記錄。"</string>

listview顯示的傳輸檔案列表,佈局xml介紹完後進入對Java檔案的分析。
@Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.bluetooth_transfers_page);
        mListView = (ListView)findViewById(R.id.list);
        mListView.setEmptyView(findViewById(R.id.empty));

        mShowAllIncoming = getIntent().getBooleanExtra(
                Constants.EXTRA_SHOW_ALL_FILES, false);

        String direction;
        int dir = getIntent().getIntExtra("direction", 0);
        if (dir == BluetoothShare.DIRECTION_OUTBOUND) {
            setTitle(getText(R.string.outbound_history_title));
            direction = "(" + BluetoothShare.DIRECTION + " == " + BluetoothShare.DIRECTION_OUTBOUND
                    + ")";
        } else {
            if (mShowAllIncoming) {
                setTitle(getText(R.string.btopp_live_folder));
            } else {
                setTitle(getText(R.string.inbound_history_title));
            }
            direction = "(" + BluetoothShare.DIRECTION + " == " + BluetoothShare.DIRECTION_INBOUND
                    + ")";
        }

        String selection = BluetoothShare.STATUS + " >= '200' AND " + direction;

        if (!mShowAllIncoming) {
            selection = selection + " AND ("
                    + BluetoothShare.VISIBILITY + " IS NULL OR "
                    + BluetoothShare.VISIBILITY + " == '"
                    + BluetoothShare.VISIBILITY_VISIBLE + "')";
        }

        final String sortOrder = BluetoothShare.TIMESTAMP + " DESC";
  mTransferCursor = managedQuery(BluetoothShare.CONTENT_URI, new String[] {
                "_id", BluetoothShare.FILENAME_HINT, BluetoothShare.STATUS,
                BluetoothShare.TOTAL_BYTES, BluetoothShare._DATA, BluetoothShare.TIMESTAMP,
                BluetoothShare.VISIBILITY, BluetoothShare.DESTINATION, BluetoothShare.DIRECTION
        }, selection, sortOrder);

        // only attach everything to the listbox if we can access
        // the transfer database. Otherwise, just show it empty
        if (mTransferCursor != null) {
            mIdColumnId = mTransferCursor.getColumnIndexOrThrow(BluetoothShare._ID);
            // Create a list "controller" for the data
            mTransferAdapter = new BluetoothOppTransferAdapter(this,
                    R.layout.bluetooth_transfer_item, mTransferCursor);
            mListView.setAdapter(mTransferAdapter);
            mListView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
            mListView.setOnCreateContextMenuListener(this);
            mListView.setOnItemClickListener(this);
        }

        mNotifier = new BluetoothOppNotification(this);
        mContextMenu = false;
    }

關鍵程式碼都在onCreate中,可以看出來通過呼叫managedQuery方法按指定的條件查詢指定的uri,獲取到cursor後傳給adapter,並將adapter與listview繫結顯示資料。

首先是對傳輸列表資料來源的獲取----managedQuery

public final Cursor managedQuery(Uri uri, String[] projection, String selection,
            String sortOrder) {
        Cursor c = getContentResolver().query(uri, projection, selection, null, sortOrder);
        if (c != null) {
            startManagingCursor(c);
        }
        return c;
    }

managedQuery方法定義在Activity中,可以看到首先獲取到ContentResolver物件,然後呼叫query方法進行查詢指定uri的資料。有幾點需要注意,通過該方法獲取到的cursor無需去呼叫close方法將其關閉,因為activity會在合適的時候將其關閉。但是有一點,如果你的cursor物件呼叫了stopManagingCursor方法時,必須手動去呼叫cursor.close方法將其關閉,因為此時,activity不會自動去關閉

需要傳入四個引數

  • uri   : the uri of the content provider to query      所要查詢的contentProvider的uri
  • projection  :  projection list of columns to return  查詢到後所要返回的cursor的列名,如果想要全部返回可以將引數置為null
  • selection  :  selectio SQL WHERE clause            查詢條件
  • sortOrder  : sortorder SQL ORDER BY clause    按某種條件排序

所查詢的uri為

/**
 * The content:// URI for the data table in the provider
 */
    public static final Uri CONTENT_URI = Uri.parse("content://com.android.bluetooth.opp/btopp");
想要讀取該uri下的資料需要在Androidmanifest.xml配置檔案中新增如下許可權
<uses-permission android:name="android.permission.ACCESS_BLUETOOTH_SHARE" />

查詢到後傳給adapter進行載入view

舉一個例子,在item上顯示遠端藍芽name的話可以使用如下程式碼

tv = (TextView)view.findViewById(R.id.targetdevice);
   //獲取到本地藍芽介面卡 
    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
       //獲取到索引 
        int destinationColumnId = cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION);
       
       //獲取到本條資料中所對應的遠端裝置
         BluetoothDevice remoteDevice = adapter.getRemoteDevice(cursor
                .getString(destinationColumnId));
        //獲取到name
        String deviceName = BluetoothOppManager.getInstance(context).getDeviceName(remoteDevice);
        tv.setText(deviceName);

顯示接收到的檔案原始碼其實實現起來不難,就是藉助contentResolver來讀取uri的資料並顯示出來,那麼資料必須要通過contentprovider的方式儲存,但是接收到的檔案時儲存在哪個contentprovider??如何進行儲存?會儲存哪些資訊?有哪些列?接下來就進行分析

首先根據uri---com.android.bluetooth.opp去查詢清單配置檔案

<provider android:name=".opp.BluetoothOppProvider"
            android:authorities="com.android.bluetooth.opp"
            android:exported="true"
            android:process="@string/process">
            <path-permission
                    android:pathPrefix="/btopp"
                    android:permission="android.permission.ACCESS_BLUETOOTH_SHARE" />
        </provider>

可以發現儲存所用的provider為BluetoothOppProvider.java,正好藉此機會分析下provider的用法。

chapter two-----儲存接收到的檔案的ContentProvider

該類位於\android\packages\apps\Bluetooth\src\com\android\bluetooth\opp資料夾下,直接繼承與ContentProvider。希望是在對contentProvider有一定的瞭解後閱讀如下分析

provider屬於元件,需要再清單配置檔案中進行配置

<provider android:name=".opp.BluetoothOppProvider"
            android:authorities="com.android.bluetooth.opp"
            android:exported="true"
            android:process="@string/process">
            <path-permission
                    android:pathPrefix="/btopp"
                    android:permission="android.permission.ACCESS_BLUETOOTH_SHARE" />
        </provider>

自定義一個provider需要新增如下屬性authorities:域名,如果需要訪問許可權,就規定所需要的訪問許可權

java程式碼中的處理如下

首先對於BluetoothOppProvider的uri進行解析

定義一個urimatcher物件,以供應用對uri進行訪問解析資料

/** URI matcher used to recognize URIs sent by applications */
    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
接下來為urimatcher物件註冊路徑,在原始碼中只註冊了兩個
/** URI matcher constant for the URI of the entire share list 返回整個資料列表*/
    private static final int SHARES = 1;

    /** URI matcher constant for the URI of an individual share 返回一條記錄*/
    private static final int SHARES_ID = 2;

    static {
        sURIMatcher.addURI("com.android.bluetooth.opp", "btopp", SHARES);
        sURIMatcher.addURI("com.android.bluetooth.opp", "btopp/#", SHARES_ID);
    }

緊接著看到原始碼中建立了資料庫,也就是說contentprovider將資料儲存到資料庫

資料庫的name為:btop.db

/** Database filename */
    private static final String DB_NAME = "btopp.db";

所建立的列包括14個,如下所示
 private void createTable(SQLiteDatabase db) {
        try {
            db.execSQL("CREATE TABLE " + DB_TABLE + "(" + BluetoothShare._ID
                    + " INTEGER PRIMARY KEY AUTOINCREMENT," + BluetoothShare.URI + " TEXT, "
                    + BluetoothShare.FILENAME_HINT + " TEXT, " + BluetoothShare._DATA + " TEXT, "
                    + BluetoothShare.MIMETYPE + " TEXT, " + BluetoothShare.DIRECTION + " INTEGER, "
                    + BluetoothShare.DESTINATION + " TEXT, " + BluetoothShare.VISIBILITY
                    + " INTEGER, " + BluetoothShare.USER_CONFIRMATION + " INTEGER, "
                    + BluetoothShare.STATUS + " INTEGER, " + BluetoothShare.TOTAL_BYTES
                    + " INTEGER, " + BluetoothShare.CURRENT_BYTES + " INTEGER, "
                    + BluetoothShare.TIMESTAMP + " INTEGER," + Constants.MEDIA_SCANNED
                    + " INTEGER); ");
        } catch (SQLException ex) {
            Log.e(TAG, "couldn't create table in downloads database");
            throw ex;
        }
    }

介紹一個小知識,在資料庫類的oncreate方法中可以直接並且只調用createTable方法,但是在更新資料庫時需要先將資料庫刪除然後再呼叫createTable建立,刪除資料庫方法如下

private void dropTable(SQLiteDatabase db) {
        try {
            db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
        } catch (SQLException ex) {
            Log.e(TAG, "couldn't drop table in downloads database");
            throw ex;
        }
    }
資料庫實在provider的oncreate方法中進行建立的

然後就會provider對資料庫進行增刪改查操作--

--------------------------

未完待續