top

Android Tutorial 第三堂(4)存取 Android 檔案系統

分享:

Android Tutorial 第三堂(3)Android 內建的 SQLite 資料庫 << 前情

Android應用程式在運作的時候,可能需要存取記憶卡或裝置內部的檔案,例如照片或影音檔,Android系統API也包含「java.io」套件,所以在一般Java應用程式用來執行檔案輸出與輸入的API,也可以在Android 應用程式中使用。要使用Android 的檔案系統之前,要先瞭解它的檔案系統分為這兩類:

  • 內部儲存設備 – 應用程式可以隨時使用這個檔案系統,存取的檔案在預設的
    情況下只有應用程式自己才可以使用;如果移除應用程式,這些檔案也會跟
    著被刪除。
  • 外部儲存設備 – 通常是記憶卡或USB 儲存設備,因為使用者可以卸載這些
    儲存設備,所以在使用前最好先判斷是否可以存取。這些設備中儲存的檔案
    是沒有保護的,任何應用程式都可以存取,所以不應該儲存機密性的資料。如果移除應用程式,只有儲存在使用「getExternalFilesDir」方法取得路徑的
    檔案,會一起被刪除;儲存在其它位置的檔案還是留在儲存設備中。

這一章說明應用程式讀取與寫入檔案的作法,這是為了後面應用程式需要儲存與讀取照片與錄音檔案的準備工作,所以只會建立一些公用的類別,並沒有增加記事應用程式的功能。

12-1 管理AVD的檔案

開始在應用程式中執行儲存設備的檔案管理之前,可以先認識如何使用ADT提
供的輔助工具,儲存與刪除在模擬裝置儲存設備中的檔案,或是從儲存設備中讀取檔案存放在作業系統中。ADT提供一個整合在Eclipse 開發工具中的「DDMS」管理工具,它可以顯示目前開發應用程式的電腦有哪一些執行中的模擬裝置,還有連接到電腦的實體裝置;也可以執行許多與模擬和實體裝置的互動,例如撥打電話給模擬裝置,讓模擬裝置接到來電;或是傳送衛星定位座標給模擬裝置,讓使用位置和地圖的應用程式可以在模擬裝置中測試。這裡會先介紹管理模擬裝置的檔案,在後面的章節也會說明使用DDMS 工具的其它用法。

先啟動Android模擬裝置,如果在Eclipse開發工具中,畫面的右上角只有「Java」的按鈕可以選擇,可以選擇Eclipse功能表「Window → Open Perspective → Other」,在出現的視窗中選擇「DDMS」,Eclipse開發工具會把開發用程式的畫面切換為DDMS 管理工具的畫面,畫面右上角也會多出一個「DDMS」的按鈕,就可以在開發應用程式的時候,選擇Java 與DDMS 切換這兩種畫面。下圖是在Eclipse開發工具中切換到DDMS 的畫面:

AndroidTutorial_03_04_01

切換到DDMS畫面後,先選擇畫面左側要執行檔案管理的模擬裝置,然後選擇
畫面右側上方的「File Explorer」標籤頁,畫面會顯示選擇模擬裝置中的檔案結構。應用程式使用的內部儲存設備位置在「/data/data/ 應用程式套件」,在這個位置下還有「files」和「cache」兩個路徑,分別存放應用程式的一般和暫存檔案。外部儲存設備在「/storage/sdcard」,這裡會有許多預設的路徑存放不同類型
的檔案。

在執行檔案管理的畫面中,右上角有幾個用來管理檔案的功能按鈕,由左到右
依照順序分別是讀取檔案儲存到作業系統(Pull File)、儲存檔案到模擬裝置(Push File)、刪除檔案(Delete)與建立目錄(New Folder)。如果需要把一個照片檔案存放到外部儲存設備,先選擇「/storage/sdcard/Pictures」,然後選擇儲存檔案的按鈕,在選擇檔案的視窗中選擇一個照片檔案,確認選擇後,檔案就會傳送到模擬裝置中儲存。

AndroidTutorial_03_04_02

使用這種方式一次只能處理一個檔案,如果需要傳送多個檔案,就要重複這些步驟完成檔案的傳送工作。如果因為測試應用程式的需求,需要一次傳送比較多檔案,使用Android開發環境提供的工具程式傳送檔案會比較方便一些。使用這種方式傳送多個檔案之前,先確認檔案在模擬裝置中存放的位置,如果需要存放在一個新的路徑,在DDMS的File Explorer中先建立好需要的路徑。例如想要在Pictures下的「icon640」存放一些應用程式測試用的照片檔案,選擇/storage/scdard/Pictures路徑後,建立一個名稱為icon640的路徑。

接下來啟動命令提示字元(Windows)或終端機(Mac OS),把路徑切換到Android工具程式的位置,如果使用ADT Bundle建立的環境,位置在「ADT Bundle解壓縮路徑/sdk/platform-tools」;如果是自己安裝的開發環境,位置在「SDK解壓縮路徑/sdk/platform-tools」。參考這個做法執行路徑的切換:

Windows作業系統:
cd C:\adt-bundle-...\sdk\platform-tools
Mac OS作業系統:
cd /adt-bundle-.../sdk/platform-tools

切換路徑以後,執行Android adb工具程式執行檔案的傳送,它的用法是「adb 存放多個照片檔案的路徑 儲存照片的路徑」。參考這個作法執行傳送多個檔案的工作:

Windows作業系統:
adb push C:\mypictures\ /storage/sdcard/Pictures/
Mac OS作業系統:
./adb push /mypictures/ /storage/sdcard/Pictures/

12-2 裝置內部的儲存設備

應用程式需要儲存一些需要的檔案到內部儲存設備,這些檔案的需求應該是經常在應用程式中存取,而且因為內部儲存設備的容量通常不會太大,所以要注意檔案的大小,如果檔案比較大的話,應該考慮使用外部儲存設備。存取Android的檔案系統,應該避免直接使用絕對路徑,Android 提供一些API 可以讓你取得儲存設備的基本路徑,如果需要儲存或讀取內部儲存設備中的檔案,應該呼叫這些宣告在Context 類別中的方法取得基本路徑:

  • getFilesDir() – 傳回的File 物件表示應用程式在內部儲存設備的路徑。
  • getCacheDir() – 傳回的File 物件表示應用程式在內部儲存設備的暫存檔案路徑,適合用來存放暫時使用的檔案,使用後應該要立即刪除。系統在儲存空間不夠的時候,可能會直接刪除這些檔案。

在Context 類別中也宣告這些用來取得儲存和讀取檔案物件的方法:

  • openFileOutput(String, int) – 第一個參數指定不包含路徑的檔案名稱;第二個參數指定「Context.MODE_PRIVATE」。呼叫這個方法後會傳回寫入檔案用的FileOutputStream 物件。
  • openFileInput(String) – 參數指定不包含路徑的檔案名稱,呼叫這個方法後傳回讀取檔案內容用的FileIntputStream 物件。

使用宣告在Context 類別的方法取得寫入和讀取的物件,都是一種處理byte 資料的輸出入串流物件,適合用來處理照片或音樂這類檔案;如果要處理字元資料的話,就需要把它們包裝為「OutputStreamWriter」和「InputStreamReader」物件。它們是java.io 套件下的API,跟一般Java 應用程式的用法是一樣的。下面這個程式片段示範寫入一些文字到內部儲存設備:

// 要儲存資料的檔案名稱
String fileName = "myfile.data";
FileOutputStream os = null;
OutputStreamWriter osw = null;

try {
    // 取得寫入檔案用的FileOutputStream物件
    os = context.openFileOutput(fileName, Context.MODE_PRIVATE);
    // 包裝為寫入字元資料的OutputStreamWriter物件
    osw = new OutputStreamWriter(os);
    // 寫入文字
    osw.write("Hello! Android!");
}
// 需要處理IOException例外
catch (IOException e) {
    Log.d(fileName, e.toString());
}
// 在finally區塊執行關閉的工作
finally {
    if (osw != null) {
        try {
            osw.close();
        }
            catch (IOException e) {
            Log.d(fileName, e.toString());
        }
    }
}

這個程式片段示範從內部儲存設備指定的檔案讀取內容的作法:

//要讀取資料的檔案名稱
String fileName = "myfile.data";
FileInputStream is = null;
InputStreamReader isr = null;
BufferedReader br = null;

try {
     // 取得讀取檔案用的FileInputStream物件
     is = context.openFileInput(fileName);
     // 包裝為讀取字元資料的InputStreamReader物件
     isr = new InputStreamReader(is);
     // 再包裝為可一次讀取一行字串資料的BufferedReader物件
     br = new BufferedReader(isr);
     // 讀取一行字串
     result = br.readLine();
}
//需要處理IOException例外
catch (IOException e) {
    Log.d(fileName, e.toString());
}
//在finally區塊執行關閉的工作
finally {
    if (is != null) {
        try {
            is.close();
        }
        catch (IOException e) {
            Log.d(fileName, e.toString());
        }
    }
}

12-3 裝置的外部儲存設備

Android系統的外部儲存設備在存取檔案的時候,會比使用內部儲存設備的效率差一些,不過它的容量通常會大一些,例如在裝置中使用的記憶卡,容量的大小通常可以支援到8或16GB以上。所以外部儲存設備適合存取比較大的檔案,例如高解析度的照片、音樂與影片檔案。

因為使用者可以卸載外部儲存設備,或是裝置本來就沒有外部儲存設備,所以在執行所有的操作前,都應該先執行外部儲存設備是否可以使用的判斷。這些判斷的工作通常會宣告為獨立的方法,在需要使用的時候會方便一些。需要判斷外部儲存設備的狀態,需要使用「andfoid.os.Environment」類別中宣告的方法與變數。這個程式片段示範執行判斷的作法:

// 外部儲存設備是否可寫入
public static boolean isExternalStorageWritable() {
    // 取得目前外部儲存設備的狀態
    String state = Environment.getExternalStorageState();

    // 判斷是否可寫入
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }

    return false;
}

// 外部儲存設備是否可讀取
public static boolean isExternalStorageReadable() {
    // 取得目前外部儲存設備的狀態
    String state = Environment.getExternalStorageState();

    // 判斷是否可讀取
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }

    return false;
}

在應用程式執行外部儲存設備檔案的寫入或讀取之前,應該先呼叫上面提供的「isExternalStorageWritable」和「isExternalStorageReadable」方法執行判斷,確定可以存取後再執行後續的工作。

外部儲存設備可以區分為公用和應用程式專用的檔案,公用的檔案可以讓所有的應用程式存取,如果使用者移除應用程式,這些檔案還是會繼續保存;應用程式中用的檔案,在使用者移除應用程式後就會跟著刪除,別的應用程式不可以存取這些檔案。存取Android的檔案系統,應該避免直接使用絕對路徑,Environment類別提供下面這些方法取得外部儲存設備的基本路徑:

  • getExternalStorageDirectory() – 傳回的File物件表示外部儲存設備的主路徑。
  • getExternalStoragePublicDirectory(String) – 傳回的File物件表示外部儲存設備的路徑,根據參數的設定可以傳回不同檔案類型的路徑。

應用程式可能需要從網際網路下載一些檔案,例如照片與音樂,Android系統為這些通用類型的檔案,在外部儲存設備中建立一些預設的路徑保存,所以應用程式也可以依照檔案的類型放在這些路徑中。所以在上面說明的方法中,可以使用宣告在Environment類別中的變數設定傳回不同檔案類型的預設路徑。下面是宣告在Environment類別中代表不同檔案類型的變數名稱:

  • DIRECTORY_MUSIC – 音樂檔案。
  • DIRECTORY_RINGTONES – 可以讓使用者選擇的來電鈴聲檔案。
  • DIRECTORY_PODCASTS – 可以讓使用者選擇的推播鈴聲檔案。
  • DIRECTORY_ALARMS – 可以讓使用者選擇的鬧鈴檔案。
  • DIRECTORY_NOTIFICATIONS – 可以讓使用者選擇的通知音效檔案。
  • DIRECTORY_PICTURES – 圖片檔案。
  • DIRECTORY_MOVIES – 影片檔案。
  • DIRECTORY_DOWNLOADS – 使用者下載的檔案。
  • DIRECTORY_DCIM – 使用者使用相機拍攝的照片與錄製的影片。

如果應用程式使用裝置的照像設備拍照或是從網路上下載一些照片,希望把這些照片儲存在公用的照片路徑下,可以使用下面的方法來取得路徑:

// 建立並傳回在公用相簿下參數指定的路徑
public static File getPublicAlbumStorageDir(String albumName) { 
    // 取得公用的照片路徑
    File pictures = Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES);
    // 準備在照片路徑下建立一個指定的路徑
    File file = new File(pictures, albumName);

    // 如果建立路徑不成功
    if (!file.mkdirs()) {
        Log.e("getAlbumStorageDir", "Directory not created");
    }

    return file;
}

使用宣告在Context類別中的「getExternalFilesDir(String)」方法,可以取得應用程式專用的外部儲存設備路徑,同樣使用宣告在Environment類別中的變數,在參數中指定需要的檔案類型路徑。如果參數指定為null值的話,就傳回應用程式專用的外部儲存設備主路徑。這個程式片段示範建立與取得應用程式專用的照片路徑:

// 建立並傳回在應用程式專用相簿下參數指定的路徑
public static File getAlbumStorageDir(Context context, String albumName) {
    // 取得應用程式專用的照片路徑
    File pictures = context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES);
    // 準備在照片路徑下建立一個指定的路徑
    File file = new File(pictures, albumName);

    // 如果建立路徑不成功
    if (!file.mkdirs()) {
        Log.e("getAlbumStorageDir", "Directory not created");
    }

    return file;
}

12-4 檔案公用類別

如果應用程式需要執行檔案的存取,例如記事應用程式儲存的照片或錄音檔案,除了使用上面介紹的基本檔案操作,另外還需要一些輔助的功能,像是產生檔案的名稱,還有設定圖片檔案給ImageView元件顯示。這些都是一般應用程式常用的功能,可以把它們撰寫在一個公用類別裡面,再讓需要的元件使用,這樣可以簡化一般元件的程式碼。

在「net.macdidi.myandroidtutorial」套件下建立「FileUtil」類別,它是一般的Java類別,裡面都是一些宣告為public與static的方法:

package net.macdidi.myandroidtutorial;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;
import android.widget.ImageView;

public class FileUtil {

    // 應用程式儲存檔案的目錄
    public static final String APP_DIR = "androidtutorial";

    // 外部儲存設備是否可寫入
    public static boolean isExternalStorageWritable() {
        // 取得目前外部儲存設備的狀態
        String state = Environment.getExternalStorageState();

        // 判斷是否可寫入
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            return true;
        }

        return false;
    }

    // 外部儲存設備是否可讀取
    public static boolean isExternalStorageReadable() {
        // 取得目前外部儲存設備的狀態
        String state = Environment.getExternalStorageState();

        // 判斷是否可讀取
        if (Environment.MEDIA_MOUNTED.equals(state) ||
            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
            return true;
        }

        return false;
    }

    // 建立並傳回在公用相簿下參數指定的路徑
    public static File getPublicAlbumStorageDir(String albumName) { 
        // 取得公用的照片路徑
        File pictures = Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES);
        // 準備在照片路徑下建立一個指定的路徑
        File file = new File(pictures, albumName);

        // 如果建立路徑不成功
        if (!file.mkdirs()) {
            Log.e("getAlbumStorageDir", "Directory not created");
        }

        return file;
    }

    // 建立並傳回在應用程式專用相簿下參數指定的路徑
    public static File getAlbumStorageDir(Context context, String albumName) {
        // 取得應用程式專用的照片路徑
        File pictures = context.getExternalFilesDir(
                Environment.DIRECTORY_PICTURES);
        // 準備在照片路徑下建立一個指定的路徑
        File file = new File(pictures, albumName);

        // 如果建立路徑不成功
        if (!file.mkdirs()) {
            Log.e("getAlbumStorageDir", "Directory not created");
        }

        return file;
    }

    // 建立並傳回外部儲存媒體參數指定的路徑
    public static File getExternalStorageDir(String dir) {
        File result = new File(
                Environment.getExternalStorageDirectory(), dir);

        if (!isExternalStorageWritable()) {
            return null;
        }

        if (!result.exists() && !result.mkdirs()) {
            return null;
        }

        return result;
    }

    // 讀取指定的照片檔案名稱設定給ImageView元件
    public static void fileToImageView(String fileName, ImageView imageView) {
        if (new File(fileName).exists()) {
            Bitmap bitmap = BitmapFactory.decodeFile(fileName);
            imageView.setImageBitmap(bitmap);
        }
        else {
            Log.e("fileToImageView", fileName + " not found.");
        }
    }

    // 產生唯一的檔案名稱
    public static String getUniqueFileName() {
        // 使用年月日_時分秒格式為檔案名稱
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
        return sdf.format(new Date());
    }    

}

12-5 應用程式使用權限設定

應用程式如果需要讀取或儲存外部儲存設備,必須在應用程式設定檔中加入授權設定,Android目前並沒有規定讀取外部儲存設備需要加入授權設定,不過為了以後的相容性,還是把它加入會好一些。為了讓後續的功能可以正確的使用檔案存取的功能,開啟應用程式設定檔案「AndroidManifest.xml」,在「mainfest」標簽裡面加入下列的授權設定:

<!-- 如果應用程式需要寫入外部儲存設備,一定要加入這個授權 -->
<uses-permission 
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 如果應用程式需要讀取外部儲存設備,最好加入這個授權 -->
<uses-permission 
android:name="android.permission.READ_EXTERNAL_STORAGE"/>

你也可以使用ADT提供的圖形介面工具設定應用程式權限,開啟應用程式設定檔案「AndroidManifest.xml」以後,切換到「Permissions」標籤:

AndroidTutorial_03_04_03

如果需要為應用程式加入新的授權設定,在這個畫面選擇「Add」按鈕:

AndroidTutorial_03_04_04

選擇「Uses Permission」後選擇「OK」按鈕:

AndroidTutorial_03_04_05

選擇需要的授權名稱以後,ADT就會自動幫你加入設定檔,切換到「AndroidManifest.xml」標籤確認後,就可以儲存檔案:

AndroidTutorial_03_04_06

完成這一章的工作了,在這裡加入的程式碼與設定,會在後續的功能使用。

課程相關的檔案都可以GitHub瀏覽與下載。
http://github.com/macdidi5/AndroidTutorial

後續 >> Android Tutorial 第四堂(1)使用照相機與麥克風

分享:
按讚!加入 CodeData Facebook 粉絲群

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

關於作者

張益裕。目前是自由講師與作者,專長是教育訓練課程規劃、教材編製與課程推廣,技術書籍與專欄寫作。涵蓋的領域有OOAD、Java程式設計、JavaFX、Java Embedded、Android與SQL。

熱門論壇文章

熱門技術文章