15 สิงหาคม 2558

[Android Code] File Observer สำหรับตรวจเช็คการเปลี่ยนแปลงของไฟล์



        ในบางแอปพลิเคชันนั้นอาจจะมีการตรวจสอบสถานะไฟล์ในเครื่อง (Storage) ว่ามีอะไรเกิดขึ้นบ้าง เพื่อที่ว่าจะให้ทำคำสั่งต่อไป ซึ่งจริงๆแล้วในแอนดรอยด์ก็จะมีคลาสที่ชื่อว่า File Observer อยู่แล้ว เพียงแต่ไม่ค่อยได้ใช้งานกันซักเท่าไรนัก

        สาเหตุที่เขียนบทความเรื่อง File Observer ก็เพราะว่าเมื่อลองไปดูวิธีการใช้งานจริงๆแล้วก็พบว่ามีอะไรหลายๆอย่างที่นักพัฒนาต้องใช้เวลาทำความเข้าใจเกี่ยวกับมัน ดังนั้นเจ้าของบล็อกจึงคิดว่าน่าสนใจดีและหยิบมาเขียนเป็นบทความให้ได้อ่านกันเล่นๆตามเคย

File Observer คืออะไร?

        เป็นคลาสที่มีไว้สำหรับคอยดักเหตุการณ์เวลาที่มีการเปลี่ยนแปลงไฟล์ใดๆใน Directory นั้นๆ ซึ่งจะมีรูปแบบการใช้งานที่ไม่ยากนัก

FileObserver observer = new FileObserver("YOUR_DIRECTORY_PATH") {
    @Override
    public void onEvent(int event, String file) {
        // Do something when event called
    }
};

        คำสั่งของ File Observer จะต้องกำหนด Directory Path ที่ต้องการ Observer โดยไม่ต้องประกาศ Permission ของ Storage จากนั้นเมื่อเกิด Event ใดๆก็ตามของ File Observer ก็จะเด้งเข้า onEvent ทันที

        แต่สิ่งที่สำคัญอย่างหนึ่งก็คือคำสั่งเพื่อให้เริ่มทำงานและหยุดทำงาน

public void startWatching();
public void stopWatching();

        ถ้าประกาศ File Observer ไว้ แต่ไม่ได้สั่งให้มันทำงาน onEvent ก็จะไม่ทำงาน ดังนั้นอย่าลืมสั่งให้เริ่มทำงานและหยุดทำงานตามที่ต้องการด้วยนะ

        ดังนั้นเมื่อใช้งานจริงก็จะประมาณนี้

import android.os.Bundle;
import android.os.FileObserver;
import android.support.v7.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        observer.startWatching();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        
        observer.stopWatching();
    }

    public FileObserver observer = new FileObserver("YOUR_DIRECTORY_PATH") {
        @Override
        public void onEvent(int event, String file) {
            // Do something when event called
        }
    };
}

        ทีนี้มาดูที่ onEvent กันต่อ ว่าจะมีอะไรให้ใช้งานได้บ้าง

ทำอะไรกับมันได้บ้าง เมื่อเกิด Event ขึ้น?

        เมื่อสังเกตที่ onEvent จะเห็นว่ามีพารามิเตอร์ส่งเข้ามาด้วยกัน 2 ตัว คือ Event (Integer) และ File Path (String)

        ซึ่ง File ที่เป็น String จะคือชื่อไฟล์ที่เกิดเหตุการณ์ขึ้น ซึ่งจะหมายถึงไฟล์ที่อยู่ใน Directory ที่กำหนดไว้ เช่น

        • ถ้าเปิดไฟล์ที่ชื่อว่า readme.txt ที่อยู่ใน Directory ดังกล่าว ค่าที่ได้ก็จะเป็น readme.txt (อิงชื่อไฟล์เลย)
        • ค่าจะเป็น Null ถ้า Event ที่เกิดขึ้นเป็นของตัว Directory เอง (ไม่ใช่ File หรือ Directory ที่อยู่ในนั้น)

        ดังนั้นถ้าอยากได้ Full Path ก็เอาไปรวมกับ Directory Path ที่กำหนดไว้ในตอนแรกละกันนะ และถ้าจะเอาไปทำอะไรก็เอา Full Path นี่แหละไปใช้งานต่ออีกที


        แต่สำหรับ Event ที่เป็น Integer นั้นจะมีผลทั้ง File และ Directory โดยจะเป็น Event Type ที่อ้างอิงจาก Constant Variable ที่อยู่ใน File Observer ดังนี้

        • FileObserver.ACCESS เมื่อ File หรือ Directory ถูกอ่านข้อมูล
        • FileObserver.ATTRIB เมื่อมีการเปลี่ยนแปลง Metadata (เช่น Permission)
        • FileObserver.CLOSE_NOWRITE เมื่อมีการปิด File หรือ Directory (เปิดไฟล์แบบ Read-only)
        • FileObserver.CLOSE_WRITE เมื่อมีการปิด File หรือ Directory (เปิดไฟล์แบบ Write)
        • FileObserver.CREATE เมื่อมีการสร้าง File หรือ Directory ขึ้นมา
        • FileObserver.DELETE เมื่อมีการลบ File หรือ Directory ทิ้ง
        • FileObserver.DELETE_SELF เมื่อลบ Directory ที่ File Observer ทำงานอยู่ทิ้ง
        • FileObserver.MODIFY เมื่อมีการแก้ไขข้อมูลใน File นั้นๆ
        • FileObserver.MOVED_FROM เมื่อมี File หรือ Directory ถูกย้ายออกไปที่อื่น
        • FileObserver.MOVED_TO เมื่อมี File หรือ Directory ถูกย้ายเข้ามา
        • FileObserver.MOVE_SELF เมื่อมี File หรือ Directory ถูกย้ายใน Directory ที่ File Observer ทำงานอยู่
        • FileObserver.OPEN เมื่อ File หรือ Directory ถูกเปิด

        และนอกจากนี้จะมีค่า Constant Variable อยู่อีกหนึ่งตัวคือ FileObserver.ALL_EVENTS ซึ่งตัวนี้ไม่ใช่ Event Type แต่ว่าจะเป็น Event Mask ที่เอาไว้กรอง Event Type อีกที เดี๋ยวจะอธิบายให้ฟังทีหลัง

วิธีการแยก Event Type ใน onEvent

        ปกติแล้ว เวลานักพัฒนาเรียกใช้งาน File Observer ก็มักจะต้องการ Event Type แค่บางอย่างเท่านั้น ไม่ได้ต้องการทั้งหมดอยู่แล้ว ดังนั้นจะมีการเช็ค Event Type เพื่อให้ได้ตามที่ต้องการแล้วค่อยเรียกใช้งานคำสั่ง

        ซึ่งการแยก Event Type จะใช้คำสั่งแบบนี้

@Override
public void onEvent(int event, String file) {
    event = event & FileObserver.ALL_EVENTS;
    
    if (event == FileObserver.CREATE) {
        // File or directory was created
    } else if (event == FileObserver.OPEN) {
        // File or directory was opened
    }
}

        หัวใจหลักอยู่ตรงที่การเอาค่า Event Type มาทำ Bitwise กับ Event Mask ก่อนที่จะเช็คเงื่อนไข เพื่อให้ได้ค่า Event Type จริงๆ ถึงจะเอาไปเช็คเงื่อนไขได้

ทำไมต้องทำ Bitwise ก่อน?

        ก่อนจะหาคำตอบว่าทำไมต้องทำ Bitwise กับ Event Mask ให้ลองดูค่า Integer ของ Event ที่ได้จาก onEvent ดูก่อน

        ยกตัวอย่างว่า Directory ที่เจ้าของบล็อกต้องการคือ /sdcard/MyApp และใน Directory นั้นเจ้าของบล็อกมีไฟล์อยู่ 1 File และ 1 Directory ดังนี้


        สมมติว่าเจ้าของบล็อกลองกำหนด File Observer ในโค๊ดไว้แบบนี้

private static final String DIR_OBSERVE = "/sdcard/MyApp/";
private FileObserver observer;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    observer = new FileObserver(DIR_OBSERVE) {
        @Override
        public void onEvent(int event, String file) {
            Log.d("File Observer", "Event " + event);
            Log.d("File Observer", "Target " + file);
        }
    };

    observer.startWatching();
}

@Override
protected void onDestroy() {
    super.onDestroy();

    observer.stopWatching();
}

        เมื่อเจ้าของบล็อกลองกดเปิด user_data จะได้ข้อมูลบน Logcat ดังนี้

D/File Observer Event : 1073741856
D/File Observer : Target user_data

        และเมื่อลองกดเปิด readme.txt จะได้ข้อมูลบน Logcat ดังนี้

D/File Observer Event : 1
D/File Observer : Target readme.txt
D/File Observer Event : 32
D/File Observer : Target readme.txt

        ทำไมค่าที่ได้จากตอนกดเปิด user_data ถึงไม่เหมือนกับ readme.txt เลยล่ะ?

        เนื่องจาก Event Type พวกนี้จะอยู่ในรูปของ Bit Flag มากกว่า ดังนั้นให้ลองแปลงเป็นเลขฐาน 2 และฐาน 16 ดูก่อน

1           ==> 0x00000001 And 0000 0000 0000 0000 0000 0000 0000 0001 
32          ==> 0x00000020 And 0000 0000 0000 0000 0000 0000 0010 0000 
1073741856  ==> 0x40000020 And 0100 0000 0000 0000 0000 0000 0010 0000 

        สำหรับ 1 เมื่อไปหา Event Type ก็จะได้เป็น FileObserver.ACCESS

        และถ้าสังเกตดีๆจะเห็นว่า 32 กับ 1073741856 มีเลขช่วงท้ายๆที่คล้ายๆกัน แต่จะมีตัวเลขช่วงข้างหน้าที่ต่างกันนิดเดียว นั่นก็คือเลขบิตที่ 30 ที่ของ user_data กลายเป็น 1 ส่วน readme.txt เป็น 0

       จึงเป็นที่มาว่าทำไมค่า Event ของ user_data ได้ออกมาเยอะโคตรๆนั่นเอง

        แล้วเลขบิตที่ 30 มันคืออะไร?

        มันคือ Is Directory Bit ที่อยู่ในไฟล์ที่ชื่อว่า inotify.h ที่เป็น Core ของ Linux นั่นเอง (แอนดรอยด์เป็น Linux Based นะเออ) สามารถดูได้ที่ inotify.h [Linux Cross Reference]

        ซึ่งบิตดังกล่าวมีไว้บอกว่า Event ที่เกิดขึ้นนั้น เกิดขึ้นกับ File หรือ Directory นั่นเอง ถ้าเป็น File บิตดังกล่าวจะมีค่าเป็น 0 แต่ถ้าเป็น Directory บิตดังกล่าวจะมีค่าเป็น 1

        จึงเป็นที่มาว่าทำไมต้องทำ Bitwise ก่อน เพื่อตัดบิตตัวนี้ทิ้งออกไปนั่นเอง


        และนอกจากจะรู้ Event Type กับ Target File แล้ว ยังสามารถเช็คได้อีกว่าเป็น File หรือ Directory โดยใช้ Flag ตัวดังกล่าวให้เป็นประโยชน์ แต่ทว่า Flag ดังกล่าวมีที่มาจาก inotify.h ที่ไม่สามารถเข้าถึงได้อยู่แล้ว ดังนั้นเจ้าของบล็อกต้องมาสร้างตัวแปรเอาไว้ใช้งานเอง

public final int FLAG_IS_DIR = 1073741824;

        ทางที่ดีใส่เป็นเลขฐาน 16 ดีกว่านะ เพราะดูแล้วเข้าใจง่ายกว่า

public final int FLAG_IS_DIR = 0x40000000;

        และเจ้าของบล็อกรู้ว่ามันเป็นบิตที่ 30 ดังนั้นบางทีเจ้าของบล็อกจึงชอบใช้วิธีแบบนี้แทน

public final int FLAG_IS_DIR = 1 << 30;

        เลือกเอาไปใช้ตามใจชอบเองละกันนะ

        ดังนั้นคำสั่งเช็คว่าเป็น File หรือ Directory จะได้ออกมาดังนี้

public final int FLAG_IS_DIR = 0x40000000;

@Override
public void onEvent(int event, String file) {
    boolean isDirEvent = (event & FLAG_IS_DIR) == FLAG_IS_DIR;
    Log.d("FileObserver Event", "Is Directory " + isDirEvent);
}

สรุปคำสั่งที่เอาไปใช้งาน

        เนื่องจากอธิบายยาวเหยียดไปหน่อย เลยขอสรุปคำสั่งสำหรับใช้งาน File Observer ใหม่อีกรอบละกัน

import android.os.Bundle;
import android.os.FileObserver;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

public class MainActivity extends AppCompatActivity {
    private FileObserver observer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        observer = new FileObserver(getFilesDir().getAbsolutePath() + "/Notes/") {
            public final int FLAG_IS_DIR = 0x40000000;

            @Override
            public void onEvent(int event, String file) {
                boolean isDirectory = (event & FLAG_IS_DIR) == FLAG_IS_DIR;
                event = event & FileObserver.ALL_EVENTS;

                if (event == FileObserver.CREATE) {
                    // Do something when file or directory was created
                } else if (event == FileObserver.OPEN) {
                    // Do something when file or directory was opened
                }
            }
        };

        observer.startWatching();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        observer.stopWatching();
    }
}

File Observer ไม่ได้ทำงานแบบ Recursive

        Observer จะทำงานแค่ Directory ที่กำหนดเท่านั้น ถ้ามี Directory ย่อยๆอยู่ในนั้นหลายๆชั้น Observer จะไม่มีผล


        จากภาพตัวอย่าง File และ Directory ที่จะมีผลกับ Observer จะมีแค่ user_data, readme.txt, settings.txt และ web เท่านั้น ถึงแม้ว่า web จะมี Directory ย่อยอยู่ในนั้น ถ้ามีการแก้ไขไฟล์ในนั้นก็จะไม่มีผลอะไรกับ Observer เพราะมันอยู่นอกเหนือขอบเขตของมัน

        ซึ่งถือว่าเป็นข้อเสียอย่างหนึ่งของ File Observer เลยก็ว่าได้ แต่ว่าเจ้าของบล็อกก็เข้าใจนะว่าทำไม เพราะถ้ามัน Recursive ได้ แล้ว Directory ที่นักพัฒนากำหนดดันมี Directory ย่อยเป็นหมื่นเป็นพันไฟล์ (เช่นกำหนดเป็น /sdcard) ก็หมายความว่า Observer ต้อง Recusive กันแหลกรานเลย นั่นก็หมายความว่าอาจจะทำให้เปลืองทรัพยากรส่วนหนึ่งไปกับ Observer โดยไม่จำเป็น

        ปัญหาดังกล่าวสามารถแก้ได้ด้วยการเขียน File Observer ที่รองรับ Recursive เอง โดยมีนักพัฒนาคนอื่นเขียนไว้ให้เรียบร้อยแล้ว ตามเข้าไปดูกันเองได้ที่ RecursiveFileObserver by masensio [GitHub] แต่เตือนไว้ก่อนนะว่าควรใช้เท่าที่จำเป็น ถ้าเป็นไปได้ก็ให้เจาะจงเฉพาะ Directory ที่ต้องการจริงๆ

Observer หยุดทำงาน เมื่อ GC (Garbage Collection) ทำงาน

        จากที่เจ้าของบล็อกทดสอบดู พบว่า File Observer สามารถทำงานได้ถึงแม้ว่าจะย่อแอปพลิเคชันแล้วก็ตาม ดังนั้นจึงทำให้สามารถทดสอบการทำงานของ File Observer ได้ง่าย เพราะไปทดสอบด้วยการใช้แอปพลิเคชันจำพวก File Explorer ทดลองดูได้ว่า Observer ทำงานตรงกับที่กำหนดไว้หรือไม่

        แต่เมื่อใดที่  GC ทำงาน ก็จะทำให้ Observer เหล่านี้หายไปด้วย หรือก็คือ "อยู่ๆ Observer ก็หยุดทำงาน" นั่นเอง ซึ่งปัญหาดังกล่าวแก้ได้ยาก เพราะ File Observer ไม่มี State การทำงานให้ตรวจสอบเลย

        ดังนั้นทางที่ดีก็ควรใช้งานเท่าที่จำเป็นก็พอนะครับ


แหล่งข้อมูลเพิ่มเติม

        • FileObserver [Android Developer]
        • RecursiveFileObserver by masensio [GitHub]





เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium