גיבוי צמדי מפתח/ערך באמצעות Android Backup Service

Android Backup Service מספק גיבוי ושחזור של אחסון בענן לנתוני מפתח/ערך באפליקציה ל-Android. במהלך פעולת גיבוי של מפתח/ערך, נתוני הגיבוי של האפליקציה מועברים לטרנספורט הגיבוי של המכשיר. אם המכשיר משתמש ב-Google Backup Transport שמוגדר כברירת מחדל, הנתונים מועברים ל-Android Backup Service לצורך העברה לארכיון.

הנתונים מוגבלים ל-5MB לכל משתמש באפליקציה. אין חיוב על אחסון נתוני הגיבוי.

בסקירה הכללית על גיבוי נתונים תוכלו לקרוא על אפשרויות הגיבוי ב-Android ולקבל הנחיות לגבי הנתונים שצריך לגבות ולשחזר.

הטמעת גיבוי של ערכי מפתח

כדי לגבות את נתוני האפליקציה, צריך להטמיע סוכן גיבוי. מנהל הגיבוי קורא לסוכן הגיבוי גם במהלך הגיבוי וגם במהלך השחזור.

כדי להטמיע סוכן גיבוי, צריך:

  1. מצהירים על סוכן הגיבוי בקובץ המניפסט באמצעות המאפיין android:backupAgent.

  2. מגדירים סוכן גיבוי באחת מהדרכים הבאות:

    • הרחבה של BackupAgent

      הכיתה BackupAgent מספקת את הממשק המרכזי שבו האפליקציה משתמשת כדי לתקשר עם מנהל הגיבוי. אם מרחיבים את המחלקה הזו ישירות, צריך לשנות את onBackup() ואת onRestore() כדי לטפל בפעולות הגיבוי והשחזור של הנתונים.

    • הרחבה של BackupAgentHelper

      הכיתה BackupAgentHelper מספקת מעטפת נוחה סביב הכיתה BackupAgent, ומצמצמת את כמות הקוד שצריך לכתוב. ב-BackupAgentHelper, אתם צריכים להשתמש באובייקט עזר אחד או יותר, שמגבים ומשחזרים באופן אוטומטי סוגים מסוימים של נתונים, כדי שלא תצטרכו להטמיע את onBackup() ו-onRestore(). אם אתם לא צריכים שליטה מלאה על הגיבויים של האפליקציה, מומלץ להשתמש ב-BackupAgentHelper כדי לטפל בגיבויים של האפליקציה.

      ב-Android יש כרגע כלי עזר לגיבוי שיגבו וישחזרו קבצים שלמים מ-SharedPreferences ומאחסון פנימי.

הצהרה על סוכן הגיבוי במניפסט

אחרי שבוחרים את שם המחלקה של סוכן הגיבוי, צריך להצהיר עליו במניפסט באמצעות המאפיין android:backupAgent בתג <application>.

לדוגמה:

<manifest ... >
    ...
    <application android:label="MyApplication"
                 android:backupAgent="MyBackupAgent">
        <meta-data android:name="com.google.android.backup.api_key"
            android:value="unused" />
        <activity ... >
            ...
        </activity>
    </application>
</manifest>

כדי לתמוך במכשירים ישנים יותר, מומלץ להוסיף את מפתח ה-API <meta-data> לקובץ המניפסט של Android. שירות הגיבוי של Android כבר לא דורש מפתח שירות, אבל יכול להיות שבמכשירים ישנים יותר עדיין תתבצע בדיקה של מפתח בזמן הגיבוי. מגדירים את android:name לערך com.google.android.backup.api_key ואת android:value לערך unused.

המאפיין android:restoreAnyVersion מקבל ערך בוליאני שמציין אם אתם רוצים לשחזר את נתוני האפליקציה, בלי קשר לגרסת האפליקציה הנוכחית בהשוואה לגרסה שיצרה את נתוני הגיבוי. ערך ברירת המחדל הוא false. מידע נוסף זמין במאמר בדיקת הגרסה של נתוני השחזור.

הרחבת BackupAgentHelper

כדי לגבות קבצים שלמים מ-SharedPreferences או מהאחסון הפנימי, צריך ליצור את סוכן הגיבוי באמצעות BackupAgentHelper. כדי ליצור סוכן גיבוי באמצעות BackupAgentHelper נדרש הרבה פחות קוד מאשר כדי להרחיב את BackupAgent, כי לא צריך להטמיע את onBackup() ואת onRestore().

ההטמעה של BackupAgentHelper צריכה להשתמש בעוזר גיבוי אחד או יותר. עוזר גיבוי הוא רכיב מיוחד ש-BackupAgentHelper מזמן כדי לבצע פעולות גיבוי ושחזור של סוג נתונים מסוים. ב-Android framework יש כרגע שני כלי עזר שונים:

אפשר לכלול כמה רכיבי עזר ב-BackupAgentHelper, אבל צריך רק רכיב עזר אחד לכל סוג נתונים. כלומר, אם יש לכם כמה קובצי SharedPreferences, אתם צריכים רק קובץ SharedPreferencesBackupHelper אחד.

לכל עוזר שרוצים להוסיף ל-BackupAgentHelper, צריך לבצע את הפעולות הבאות במהלך השיטה onCreate():

  1. יוצרים מופע של מחלקת העזר שנבחרה. ב-constructor של המחלקה, צריך לציין את הקבצים שרוצים לגבות.
  2. מתקשרים אל addHelper() כדי להוסיף את העוזר או העוזרת ל-BackupAgentHelper.

בקטעים הבאים מוסבר איך ליצור סוכן גיבוי באמצעות כל אחד מהכלים הזמינים.

גיבוי SharedPreferences

כשיוצרים מופע של SharedPreferencesBackupHelper, צריך לכלול את השם של קובץ SharedPreferences אחד או יותר.

לדוגמה, כדי לגבות קובץ SharedPreferences בשם user_preferences, סוכן גיבוי מלא שמשתמש ב-BackupAgentHelper נראה כך:

Kotlin

// The name of the SharedPreferences file
const val PREFS = "user_preferences"

// A key to uniquely identify the set of backup data
const val PREFS_BACKUP_KEY = "prefs"

class MyPrefsBackupAgent : BackupAgentHelper() {
    override fun onCreate() {
        // Allocate a helper and add it to the backup agent
        SharedPreferencesBackupHelper(this, PREFS).also {
            addHelper(PREFS_BACKUP_KEY, it)
        }
    }
}

Java

public class MyPrefsBackupAgent extends BackupAgentHelper {
    // The name of the SharedPreferences file
    static final String PREFS = "user_preferences";

    // A key to uniquely identify the set of backup data
    static final String PREFS_BACKUP_KEY = "prefs";

    // Allocate a helper and add it to the backup agent
    @Override
    public void onCreate() {
        SharedPreferencesBackupHelper helper =
                new SharedPreferencesBackupHelper(this, PREFS);
        addHelper(PREFS_BACKUP_KEY, helper);
    }
}

ה-SharedPreferencesBackupHelper כולל את כל הקוד שנדרש כדי לגבות ולשחזר קובץ SharedPreferences.

כשמנהל הגיבוי קורא ל-onBackup() ול-onRestore(), BackupAgentHelper קורא לעוזרי הגיבוי כדי לגבות ולשחזר את הקבצים שציינתם.

גיבוי קבצים אחרים

כשיוצרים מופע של FileBackupHelper, צריך לכלול את השם של קובץ אחד או יותר שנשמר באחסון הפנימי של האפליקציה, כפי שצוין על ידי getFilesDir(), שהוא אותו מיקום שבו openFileOutput() כותב קבצים.

לדוגמה, כדי לגבות שני קבצים בשם scores ו-stats, סוכן גיבוי שמשתמש ב-BackupAgentHelper נראה כך:

Kotlin

// The name of the file
const val TOP_SCORES = "scores"
const val PLAYER_STATS = "stats"
// A key to uniquely identify the set of backup data
const val FILES_BACKUP_KEY = "myfiles"

class MyFileBackupAgent : BackupAgentHelper() {
    override fun onCreate() {
        // Allocate a helper and add it to the backup agent
        FileBackupHelper(this, TOP_SCORES, PLAYER_STATS).also {
            addHelper(FILES_BACKUP_KEY, it)
        }
    }
}

Java

public class MyFileBackupAgent extends BackupAgentHelper {
    // The name of the file
    static final String TOP_SCORES = "scores";
    static final String PLAYER_STATS = "stats";

    // A key to uniquely identify the set of backup data
    static final String FILES_BACKUP_KEY = "myfiles";

    // Allocate a helper and add it to the backup agent
    @Override
    public void onCreate() {
        FileBackupHelper helper = new FileBackupHelper(this,
                TOP_SCORES, PLAYER_STATS);
        addHelper(FILES_BACKUP_KEY, helper);
    }
}

‫FileBackupHelper כולל את כל הקוד שנדרש כדי לגבות ולשחזר קבצים שנשמרו באחסון הפנימי של האפליקציה.

עם זאת, קריאה וכתיבה של קבצים באחסון הפנימי לא בטוחה ל-thread. כדי לוודא שסוכן הגיבוי לא קורא או כותב את הקבצים שלכם במקביל לפעילויות שלכם, אתם צריכים להשתמש בהצהרות מסונכרנות בכל פעם שאתם מבצעים קריאה או כתיבה. לדוגמה, בכל פעילות שבה קוראים וכותבים את הקובץ, צריך אובייקט שישמש כנעילה פנימית להצהרות המסונכרנות:

Kotlin

// Object for intrinsic lock
companion object {
    val sDataLock = Any()
}

Java

// Object for intrinsic lock
static final Object sDataLock = new Object();

לאחר מכן, צרו הצהרה מסונכרנת עם הנעילה הזו בכל פעם שאתם קוראים את הקבצים או כותבים בהם. לדוגמה, הנה הצהרה מסונכרנת לכתיבת הניקוד האחרון במשחק בקובץ:

Kotlin

try {
    synchronized(MyActivity.sDataLock) {
        val dataFile = File(filesDir, TOP_SCORES)
        RandomAccessFile(dataFile, "rw").apply {
            writeInt(score)
        }
    }
} catch (e: IOException) {
    Log.e(TAG, "Unable to write to file")
}

Java

try {
    synchronized (MyActivity.sDataLock) {
        File dataFile = new File(getFilesDir(), TOP_SCORES);
        RandomAccessFile raFile = new RandomAccessFile(dataFile, "rw");
        raFile.writeInt(score);
    }
} catch (IOException e) {
    Log.e(TAG, "Unable to write to file");
}

צריך לסנכרן את הצהרות הקריאה עם אותה נעילה.

לאחר מכן, ב-BackupAgentHelper, צריך לשנות את onBackup() ואת onRestore() כדי לסנכרן את פעולות הגיבוי והשחזור עם אותו מנעול פנימי. לדוגמה, בדוגמה הקודמת של MyFileBackupAgent נדרשות השיטות הבאות:

Kotlin

@Throws(IOException::class)
override fun onBackup(
        oldState: ParcelFileDescriptor,
        data: BackupDataOutput,
        newState: ParcelFileDescriptor
) {
    // Hold the lock while the FileBackupHelper performs back up
    synchronized(MyActivity.sDataLock) {
        super.onBackup(oldState, data, newState)
    }
}

@Throws(IOException::class)
override fun onRestore(
        data: BackupDataInput,
        appVersionCode: Int,
        newState: ParcelFileDescriptor
) {
    // Hold the lock while the FileBackupHelper restores the file
    synchronized(MyActivity.sDataLock) {
        super.onRestore(data, appVersionCode, newState)
    }
}

Java

@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
          ParcelFileDescriptor newState) throws IOException {
    // Hold the lock while the FileBackupHelper performs back up
    synchronized (MyActivity.sDataLock) {
        super.onBackup(oldState, data, newState);
    }
}

@Override
public void onRestore(BackupDataInput data, int appVersionCode,
        ParcelFileDescriptor newState) throws IOException {
    // Hold the lock while the FileBackupHelper restores the file
    synchronized (MyActivity.sDataLock) {
        super.onRestore(data, appVersionCode, newState);
    }
}

הרחבת BackupAgent

ברוב האפליקציות לא צריך להרחיב את המחלקה BackupAgent באופן ישיר, אלא להרחיב את BackupAgentHelper כדי לנצל את מחלקות העזר המובנות שמגבות ומשחזרות את הקבצים באופן אוטומטי. עם זאת, אתם יכולים להרחיב את BackupAgent ישירות כדי לבצע את הפעולות הבאות:

  • הגדירו גרסה לפורמט הנתונים. לדוגמה, אם אתם צופים שתצטרכו לשנות את הפורמט שבו אתם כותבים את נתוני האפליקציה, אתם יכולים ליצור סוכן גיבוי שיבצע הצלבה של גרסת האפליקציה במהלך פעולת שחזור, ויבצע את כל פעולות התאימות הנדרשות אם הגרסה במכשיר שונה מזו של נתוני הגיבוי. מידע נוסף זמין במאמר בדיקת הגרסה של נתוני השחזור.
  • מציינים את חלקי הנתונים שרוצים לגבות. במקום לגבות קובץ שלם, אתם יכולים לציין את חלקי הנתונים שצריך לגבות ואיך כל חלק ישוחזר במכשיר. זה גם יכול לעזור לכם לנהל גרסאות שונות, כי אתם קוראים וכותבים את הנתונים כישויות ייחודיות, ולא כקבצים שלמים.
  • גיבוי נתונים במסד נתונים. אם יש לכם מסד נתונים של SQLite שאתם רוצים לשחזר כשהמשתמש מתקין מחדש את האפליקציה, אתם צריכים ליצור BackupAgent בהתאמה אישית שקורא את הנתונים המתאימים במהלך פעולת גיבוי, ואז ליצור את הטבלה ולהוסיף את הנתונים במהלך פעולת שחזור.

אם אתם לא צריכים לבצע אף אחת מהמשימות הקודמות ואתם רוצים לגבות קבצים שלמים מ-SharedPreferences או מאחסון פנימי, קראו את המאמר הרחבת BackupAgentHelper.

השיטות הנדרשות

כשיוצרים BackupAgent, צריך להטמיע את שיטות הקריאה החוזרת הבאות:

onBackup()
מנהל הגיבויים קורא ל-method הזו אחרי שמבקשים גיבוי. בשיטה הזו, קוראים את נתוני האפליקציה מהמכשיר ומעבירים את הנתונים שרוצים לגבות למנהל הגיבוי, כמו שמתואר בקטע ביצוע גיבוי.
onRestore()

מנהל הגיבויים מפעיל את השיטה הזו במהלך פעולת שחזור. השיטה הזו מספקת את נתוני הגיבוי, שבהם האפליקציה יכולה להשתמש כדי לשחזר את המצב הקודם שלה, כפי שמתואר בקטע ביצוע שחזור.

המערכת קוראת לשיטה הזו כדי לשחזר את נתוני הגיבוי כשהמשתמש מתקין מחדש את האפליקציה, אבל האפליקציה יכולה גם לבקש שחזור.

ביצוע גיבוי

בקשת גיבוי לא גורמת לקריאה מיידית ל-method‏ onBackup(). במקום זאת, מנהל הגיבויים ממתין לזמן מתאים ואז מבצע גיבוי לכל האפליקציות שביקשו גיבוי מאז הגיבוי האחרון שבוצע. בשלב הזה צריך לספק את נתוני האפליקציה למנהל הגיבוי כדי שיוכל לשמור אותם באחסון בענן.

רק מנהל הגיבוי יכול לקרוא ל-method onBackup() של סוכן הגיבוי. בכל פעם שנתוני האפליקציה משתנים ואתם רוצים לבצע גיבוי, אתם צריכים לבקש פעולת גיבוי באמצעות קריאה ל-dataChanged(). מידע נוסף זמין במאמר בקשת גיבוי.

טיפ: בזמן פיתוח האפליקציה, אפשר להפעיל פעולת גיבוי מיידית ממנהל הגיבוי באמצעות הכלי bmgr.

כשמנהל הגיבוי קורא ל-method‏ onBackup(), הוא מעביר שלושה פרמטרים:

oldState
ParcelFileDescriptor פתוח לקריאה בלבד שמצביע על מצב הגיבוי האחרון שסופק על ידי האפליקציה. אלה לא נתוני הגיבוי מאחסון בענן, אלא ייצוג מקומי של הנתונים שגובו בפעם האחרונה שבה בוצעה קריאה ל-onBackup(), כפי שהוגדר על ידי newState או מ-onRestore(). הסבר על onRestore() מופיע בקטע הבא. מכיוון ש-onBackup() לא מאפשר לקרוא נתוני גיבוי קיימים באחסון בענן, אפשר להשתמש בייצוג המקומי הזה כדי לקבוע אם הנתונים השתנו מאז הגיבוי האחרון.
data
אובייקט BackupDataOutput שמשמש להעברת נתוני הגיבוי אל Backup Manager.
newState
ParcelFileDescriptor פתוח לקריאה ולכתיבה שמפנה לקובץ שבו צריך לכתוב ייצוג של הנתונים שהעברתם אל data. ייצוג יכול להיות חותמת הזמן של השינוי האחרון בקובץ. האובייקט הזה מוחזר בתור oldState בפעם הבאה שמנהל הגיבוי קורא ל-method‏ onBackup(). אם לא כותבים את נתוני הגיבוי ב-newState, אז oldState יצביע על קובץ ריק בפעם הבאה שמנהל הגיבויים יקרא ל-onBackup().

בעזרת הפרמטרים האלה, מטמיעים את השיטה onBackup() כדי לבצע את הפעולות הבאות:

  1. כדי לבדוק אם הנתונים השתנו מאז הגיבוי האחרון, משווים בין oldState לבין הנתונים הנוכחיים. האופן שבו קוראים נתונים ב-oldState תלוי באופן שבו כתבתם אותם במקור ב-newState (ראו שלב 3). הדרך הקלה ביותר לתעד את מצב הקובץ היא באמצעות חותמת הזמן של השינוי האחרון. לדוגמה, כך אפשר לקרוא ולהשוות חותמת זמן מ-oldState:

    Kotlin

    val instream = FileInputStream(oldState.fileDescriptor)
    val dataInputStream = DataInputStream(instream)
    try {
        // Get the last modified timestamp from the state file and data file
        val stateModified = dataInputStream.readLong()
        val fileModified: Long = dataFile.lastModified()
        if (stateModified != fileModified) {
            // The file has been modified, so do a backup
            // Or the time on the device changed, so be safe and do a backup
        } else {
            // Don't back up because the file hasn't changed
            return
        }
    } catch (e: IOException) {
        // Unable to read state file... be safe and do a backup
    }
    

    Java

    // Get the oldState input stream
    FileInputStream instream = new FileInputStream(oldState.getFileDescriptor());
    DataInputStream in = new DataInputStream(instream);
    
    try {
        // Get the last modified timestamp from the state file and data file
        long stateModified = in.readLong();
        long fileModified = dataFile.lastModified();
    
        if (stateModified != fileModified) {
            // The file has been modified, so do a backup
            // Or the time on the device changed, so be safe and do a backup
        } else {
            // Don't back up because the file hasn't changed
            return;
        }
    } catch (IOException e) {
        // Unable to read state file... be safe and do a backup
    }
    

    אם לא השתנה שום דבר ואין צורך לגבות, דלגו לשלב 3.

  2. אם הנתונים השתנו בהשוואה ל-oldState, כותבים את הנתונים הנוכחיים ב-data כדי לגבות אותם באחסון בענן.

    צריך לכתוב כל מקטע נתונים כישות ב-BackupDataOutput. ישות היא רשומת נתונים בינאריים שטוחה שמזוהה באמצעות מחרוזת מפתח ייחודית. לכן, קבוצת הנתונים שאתם מגבים היא מבחינה רעיונית קבוצה של צמדי מפתח/ערך.

    כדי להוסיף ישות למערך הנתונים של הגיבוי, צריך:

    1. קוראים ל-writeEntityHeader() ומעבירים מפתח מחרוזת ייחודי לנתונים שרוצים לכתוב ואת גודל הנתונים.

    2. קוראים ל-writeEntityData() ומעבירים מאגר נתונים זמני של בייטים שמכיל את הנתונים ואת מספר הבייטים שצריך לכתוב מהמאגר הזמני. המספר הזה צריך להיות זהה לגודל שהועבר ל-writeEntityHeader().

    לדוגמה, הקוד הבא משטח נתונים מסוימים לזרם בייטים וכותב אותם לישות אחת:

    Kotlin

    val buffer: ByteArray = ByteArrayOutputStream().run {
        DataOutputStream(this).apply {
            writeInt(playerName)
            writeInt(playerScore)
        }
        toByteArray()
    }
    val len: Int = buffer.size
    data.apply {
        writeEntityHeader(TOPSCORE_BACKUP_KEY, len)
        writeEntityData(buffer, len)
    }
    

    Java

    // Create buffer stream and data output stream for our data
    ByteArrayOutputStream bufStream = new ByteArrayOutputStream();
    DataOutputStream outWriter = new DataOutputStream(bufStream);
    // Write structured data
    outWriter.writeUTF(playerName);
    outWriter.writeInt(playerScore);
    // Send the data to the Backup Manager using the BackupDataOutput
    byte[] buffer = bufStream.toByteArray();
    int len = buffer.length;
    data.writeEntityHeader(TOPSCORE_BACKUP_KEY, len);
    data.writeEntityData(buffer, len);
    

    מבצעים את הפעולה הזו לכל פריט נתונים שרוצים לגבות. אתם יכולים להחליט איך לחלק את הנתונים לישויות. יכול להיות שתשתמשו רק בישות אחת.

  3. בין אם אתם מבצעים גיבוי (בשלב 2) ובין אם לא, כותבים ייצוג של הנתונים הנוכחיים ב-newState ParcelFileDescriptor. מנהל הגיבוי שומר את האובייקט הזה באופן מקומי כייצוג של הנתונים שמגובים כרגע. הוא מעביר את הערך הזה בחזרה אליכם בתור oldState בפעם הבאה שהוא קורא ל-onBackup(), כדי שתוכלו לקבוע אם יש צורך בגיבוי נוסף, כפי שמתואר בשלב 1. אם לא תכתבו את מצב הנתונים הנוכחי בקובץ הזה, oldState יהיה ריק במהלך הקריאה החוזרת הבאה.

    בדוגמה הבאה, ייצוג של הנתונים הנוכחיים נשמר ב-newState באמצעות חותמת הזמן של השינוי האחרון בקובץ:

    Kotlin

    val modified = dataFile.lastModified()
    FileOutputStream(newState.fileDescriptor).also {
        DataOutputStream(it).apply {
            writeLong(modified)
        }
    }
    

    Java

    FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
    DataOutputStream out = new DataOutputStream(outstream);
    
    long modified = dataFile.lastModified();
    out.writeLong(modified);
    

ביצוע שחזור

כשיגיע הזמן לשחזר את נתוני האפליקציה, מנהל הגיבוי יפעיל את השיטה onRestore() של סוכן הגיבוי. כשהוא קורא לשיטה הזו, מנהל הגיבוי מעביר את נתוני הגיבוי כדי שתוכלו לשחזר אותם במכשיר.

רק מנהל הגיבוי יכול להפעיל את onRestore(), וזה קורה באופן אוטומטי כשהמערכת מתקינה את האפליקציה ומוצאת נתוני גיבוי קיימים.

כשמנהל הגיבוי קורא ל-method‏ onRestore(), הוא מעביר שלושה פרמטרים:

data
אובייקט BackupDataInput, שמאפשר לקרוא את נתוני הגיבוי.
appVersionCode
מספר שלם שמייצג את הערך של מאפיין המניפסט android:versionCode של האפליקציה, כפי שהיה כשהנתונים האלה גובו. אתם יכולים להשתמש בזה כדי להצליב את גרסת האפליקציה הנוכחית ולקבוע אם פורמט הנתונים תואם. למידע נוסף על השימוש בזה כדי לטפל בגרסאות שונות של נתוני שחזור, ראו בדיקת הגרסה של נתוני השחזור.
newState
‫ParcelFileDescriptor פתוח לקריאה ולכתיבה שמצביע על קובץ שבו צריך לכתוב את מצב הגיבוי הסופי שסופק עם data. האובייקט הזה מוחזר בתור oldState בפעם הבאה שמתבצעת קריאה ל-onBackup(). חשוב לזכור שצריך גם לכתוב את אותו אובייקט newState בקריאה החוזרת (callback) של onBackup(). אם עושים את זה גם כאן, זה מבטיח שאובייקט oldState שניתן ל-onBackup() יהיה תקין גם בפעם הראשונה שבה מתבצעת קריאה ל-onBackup() אחרי שחזור המכשיר.

בהטמעה של onRestore(), צריך להפעיל את readNextHeader() ב-data כדי לעבור על כל הישויות בקבוצת הנתונים. לכל ישות שנמצאה, מבצעים את הפעולות הבאות:

  1. משיגים את מפתח הישות באמצעות getKey().
  2. משווים את מפתח הישות לרשימה של ערכי מפתחות ידועים, שאותם צריך להצהיר כמחרוזות סטטיות סופיות בתוך המחלקה BackupAgent. כשהמפתח תואם לאחת ממחרוזות המפתחות המוכרות, מזינים הצהרה כדי לחלץ את נתוני הישות ולשמור אותם במכשיר:

    1. מקבלים את גודל נתוני הישות באמצעות getDataSize() ויוצרים מערך בייטים בגודל הזה.
    2. מפעילים את readEntityData() ומעבירים אליה את מערך הבייטים, שבו הנתונים יאוחסנו, ומציינים את היסט ההתחלה ואת הגודל לקריאה.
    3. מערך הבייטים מלא. קוראים את הנתונים וכותבים אותם במכשיר איך שרוצים.
  3. אחרי שקוראים את הנתונים וכותבים אותם בחזרה למכשיר, כותבים את מצב הנתונים בפרמטר newState, כמו שעושים במהלך onBackup().

לדוגמה, כך אפשר לשחזר את הנתונים שגובו בדוגמה שבקטע הקודם:

Kotlin

@Throws(IOException::class)
override fun onRestore(data: BackupDataInput, appVersionCode: Int,
                       newState: ParcelFileDescriptor) {
    with(data) {
        // There should be only one entity, but the safest
        // way to consume it is using a while loop
        while (readNextHeader()) {
            when(key) {
                TOPSCORE_BACKUP_KEY -> {
                    val dataBuf = ByteArray(dataSize).also {
                        readEntityData(it, 0, dataSize)
                    }
                    ByteArrayInputStream(dataBuf).also {
                        DataInputStream(it).apply {
                            // Read the player name and score from the backup data
                            playerName = readUTF()
                            playerScore = readInt()
                        }
                        // Record the score on the device (to a file or something)
                        recordScore(playerName, playerScore)
                    }
                }
                else -> skipEntityData()
            }
        }
    }

    // Finally, write to the state blob (newState) that describes the restored data
    FileOutputStream(newState.fileDescriptor).also {
        DataOutputStream(it).apply {
            writeUTF(playerName)
            writeInt(mPlayerScore)
        }
    }
}

Java

@Override
public void onRestore(BackupDataInput data, int appVersionCode,
                      ParcelFileDescriptor newState) throws IOException {
    // There should be only one entity, but the safest
    // way to consume it is using a while loop
    while (data.readNextHeader()) {
        String key = data.getKey();
        int dataSize = data.getDataSize();

        // If the key is ours (for saving top score). Note this key was used when
        // we wrote the backup entity header
        if (TOPSCORE_BACKUP_KEY.equals(key)) {
            // Create an input stream for the BackupDataInput
            byte[] dataBuf = new byte[dataSize];
            data.readEntityData(dataBuf, 0, dataSize);
            ByteArrayInputStream baStream = new ByteArrayInputStream(dataBuf);
            DataInputStream in = new DataInputStream(baStream);

            // Read the player name and score from the backup data
            playerName = in.readUTF();
            playerScore = in.readInt();

            // Record the score on the device (to a file or something)
            recordScore(playerName, playerScore);
        } else {
            // We don't know this entity key. Skip it. (Shouldn't happen.)
            data.skipEntityData();
        }
    }

    // Finally, write to the state blob (newState) that describes the restored data
    FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
    DataOutputStream out = new DataOutputStream(outstream);
    out.writeUTF(playerName);
    out.writeInt(mPlayerScore);
}

בדוגמה הזו, הפרמטר appVersionCode שמועבר אל onRestore() לא נמצא בשימוש. עם זאת, יכול להיות שתרצו להשתמש בו אם בחרתם לבצע גיבוי כשגרסת האפליקציה של המשתמש למעשה חזרה אחורה (לדוגמה, המשתמש עבר מגרסה 1.5 של האפליקציה לגרסה 1.0). מידע נוסף מופיע בקטע הבא.

בדיקת הגרסה של הנתונים לשחזור

כשהמנהל של הגיבוי שומר את הנתונים שלכם באחסון בענן, הוא כולל באופן אוטומטי את גרסת האפליקציה, כפי שהיא מוגדרת במאפיין android:versionCode של קובץ המניפסט. לפני שמנהל הגיבוי קורא לסוכן הגיבוי כדי לשחזר את הנתונים, הוא בודק את android:versionCode של האפליקציה המותקנת ומשווה אותו לערך שרשום בקבוצת הנתונים לשחזור. אם הגרסה שמתועדת בקבוצת הנתונים לשחזור חדשה יותר מגרסת האפליקציה במכשיר, המשתמש שדרג לאחור את האפליקציה. במקרה כזה, מנהל הגיבוי יבטל את פעולת השחזור של האפליקציה ולא יפעיל את השיטה onRestore(), כי קבוצת הנתונים לשחזור נחשבת חסרת משמעות לגרסה ישנה יותר.

אפשר לשנות את ההתנהגות הזו באמצעות המאפיין android:restoreAnyVersion:

  • ‫false (ברירת המחדל): מנהל הגיבוי מבטל את פעולת השחזור אם הגרסה שמתועדת בקבוצת הנתונים לשחזור חדשה יותר מגרסת האפליקציה במכשיר.
  • true: מנהל הגיבוי מתעלם מ-android:versionCode וקורא ל-onRestore() בכל המקרים, כך שאפשר לבדוק באופן ידני אם יש הבדלים בין הגרסאות ולהתאים את הנתונים.

כדי לעזור לכם לטפל בגרסאות שונות במהלך פעולת שחזור, השיטה onRestore() מעבירה לכם את קוד הגרסה שכלול במערך הנתונים לשחזור בתור הפרמטר appVersionCode. אחר כך אפשר לשלוח שאילתה לגבי קוד הגרסה של האפליקציה הנוכחית באמצעות השדה PackageInfo.versionCode. לדוגמה:

Kotlin

val info: PackageInfo? = try {
    packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
    null
}

val version: Int = info?.versionCode ?: 0

Java

PackageInfo info;
try {
    String name = getPackageName();
    info = getPackageManager().getPackageInfo(name, 0);
} catch (NameNotFoundException nnfe) {
    info = null;
}

int version;
if (info != null) {
    version = info.versionCode;
}

אחר כך משווים בין הערך של version שהתקבל מ-PackageInfo לבין הערך של appVersionCode שהועבר אל onRestore().

בקשת גיבוי

אפשר לבקש פעולת גיבוי בכל שלב באמצעות קריאה ל-dataChanged(). השיטה הזו מודיעה למנהל הגיבוי שאתם רוצים לגבות את הנתונים באמצעות סוכן הגיבוי. לאחר מכן, מנהל הגיבוי קורא ל-method‏ onBackup() של סוכן הגיבוי במועד עתידי. בדרך כלל, כדאי לבקש גיבוי בכל פעם שהנתונים משתנים (למשל, כשהמשתמש משנה העדפה באפליקציה שרוצים לגבות). אם קוראים ל-dataChanged() כמה פעמים לפני שמנהל הגיבוי מבקש גיבוי מהסוכן, הסוכן עדיין מקבל רק קריאה אחת ל-onBackup().

בקשה לשחזור

במהלך מחזור החיים הרגיל של האפליקציה, לא אמור להיות צורך לבקש פעולת שחזור. המערכת בודקת באופן אוטומטי אם יש נתוני גיבוי ומבצעת שחזור כשהאפליקציה מותקנת.

מעבר לגיבוי אוטומטי

כדי להעביר את האפליקציה לגיבויים של נתונים מלאים, צריך להגדיר את android:fullBackupOnly לערך true ברכיב <application> בקובץ המניפסט. אם האפליקציה פועלת במכשיר עם Android 5.1 (רמת API‏ 22) או גרסה ישנה יותר, היא מתעלמת מהערך הזה בקובץ המניפסט וממשיכה לבצע גיבויים של מפתח/ערך. כשמפעילים את האפליקציה במכשיר עם Android בגרסה 6.0 (רמת API‏ 23) ומעלה, היא מבצעת גיבוי אוטומטי במקום גיבוי של ערכי מפתח.

פרטיות המשתמשים

ב-Google, אנחנו מודעים היטב לאמון שהמשתמשים נותנים בנו ולאחריות שלנו להגן על הפרטיות שלהם. כדי לספק תכונות גיבוי ושחזור, Google מעבירה באופן מאובטח נתוני גיבוי אל השרתים שלה ומהם. ‫Google מתייחסת לנתונים האלה בתור מידע אישי בהתאם למדיניות הפרטיות שלה.

בנוסף, המשתמשים יכולים להשבית את פונקציונליות הגיבוי של הנתונים דרך הגדרות הגיבוי של מערכת Android. כשמשתמש משבית את הגיבוי, Android Backup Service מוחק את כל נתוני הגיבוי שנשמרו. המשתמש יכול להפעיל מחדש את הגיבוי במכשיר, אבל Android Backup Service לא ישחזר נתונים שנמחקו בעבר.