やりたいこと
ExpoのデフォルトのAndroidManifest.xmlではapplicationのandroid:allowBackup
がtrue
で、つまり自動バックアップが有効になっています。
[追記(2020/08/20)]
SDK38からallowBackupの設定はできるようになっています。
https://blog.expo.io/expo-sdk-38-is-now-available-ab6cd30ca2ee#d757
訳あってこれをfalse
にしたい。
しかしExpoのManaged WorkflowではiOSのinfoPlistのようにはAndroidManifest.xmlの詳細項目を直接設定することはできないようで、Feature requestsとしてもいくつか挙がっています。
https://expo.canny.io/feature-requests?search=allowbackup
そのため、ビルド後のAPKファイルを展開してAndroidManifest.xmlを編集、再ビルドという手順で無理矢理いじってみることにしました。Javaのソースコードはともかく、マニフェストファイルくらいは簡単に書き換えられるだろうということで。
あまり聞こえのいい感じはしませんが、とりあえずできるかどうか試してみたという感じです。
まずはallowBackup="true"
の挙動を確認
こんな感じで入力したテキストを保存するような画面を簡単に作ってみます。
バックアップはAsyncStorageで保存したデータなども含むはず。
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, AsyncStorage, Button, TextInput } from 'react-native';
export default function App() {
const [text, setText] = useState('');
useEffect(() => {
AsyncStorage.getItem('text').then(setText);
}, []);
const saveText = () => {
AsyncStorage.setItem('text', text);
};
return (
<View style={styles.container}>
<TextInput style={styles.input} value={text} onChangeText={setText} />
<Button title="Save" onPress={saveText} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
input: {
backgroundColor: 'lightgray',
width: 200,
height: 40,
marginBottom: 16
}
});
app.json
にパッケージ名を設定したうえでAndroid向けにビルドし、Android端末で確認してみます。
何かしら入力したあと「SAVE」ボタンを押し、AsyncStorageに保存させます。再度起動しても同じ文字列が表示されるようになるはずです。
この状態で、adbを使ってバックアップをしてみます。
(この辺りのドキュメントや記事を参考にしています。)
$ adb shell setprop log.tag.BackupXmlParserLogging VERBOSE
$ adb shell bmgr run
$ adb shell bmgr backupnow <パッケージ名>
バックアップができたら、先ほど入力した文字列を消して「SAVE」し、
再度起動しても入力した文字列が消えているのを確認します。
ここで先ほどバックアップした状態を復元してみます。
バックアップを特定するためのトークンを確認するコマンドを叩きましょう。
$ adb shell dumpsys backup
ずらっと出てきた中に、Current: ~~
とあるのがバックアップトークンです。よほど時間が経っていない限りこれが先ほど行ったバックアップのものなので、
$ adb shell bmgr restore <バックアップトークン> <パッケージ名>
こんな感じで叩けば復元が完了するはずです。
再度アプリを開いてみると、文字列が復活しました。
バックアップした時のAsyncStorageのデータが復元されているのがわかります。
これでallowBackup="true"
が正常に効いているのがわかりました。
ビルド後のAPKをいじってみる
ビルド後のAPKを展開し、allowBackup="false"
に修正してみます。
APKの展開には、apktoolを使用しますが、Expoの開発フローとある程度調和させたいなとか、インストールが面倒だなということで、Node.jsでシェルスクリプトっぽいのを書いてみました。
.jarのライブラリファイルをそのまま含むnpmパッケージがあるので、これを使います。
$ yarn add --dev child_process apktool-jar replace
ExpoでビルドしたAPKファイルを、ディレクトリを切ってdist/app.apk
としてプロジェクトディレクトリに移動するような感じを想定しています。
const childProcess = require('child_process');
const apktool = require('apktool-jar');
const replace = require('replace');
const directory = 'dist/'; // APKを入れるディレクトリ
const srcApkName = 'app.apk'; // 元のAPKファイル名
const distDirectory = 'dist/app/'; // 展開するディレクトリ
const revisedApkName = 'app-revised.apk'; // 再ビルドしたAPKファイル名
const distApkName = 'app-release.apk'; // 最適化・署名したAPKファイル名
const keystorePath = '/Users/.../.android/test.keystore'; // keystoreのパス
const keyAlias = 'TEST'; // keystoreのエイリアス
const keyPass = 'password'; // keystoreのパスワード
/**
* spawnをpromise化
* @param {string} command
* @param {Array<string>} args
* @returns {Promise<any>}
*/
const spawn = (command, args) => (
new Promise((resolve)=>{
const process = childProcess.spawn(command, args);
process.stdout.on('data', (data) => console.log(`${data}`));
process.stderr.on('data', (data) => console.log(`${data}`));
process.on('exit', resolve);
})
);
(async () => {
// apktoolでAPKを展開
await spawn('java', ['-jar', apktool.path, 'd', `${directory}${srcApkName}`, '-o', distDirectory, '-f']);
// 展開されたAndroidManifest.xmlをいじる
// allowBackupをfalseに変更
replace({
regex: 'android:allowBackup="true"',
replacement: 'android:allowBackup="false"',
paths: [`${distDirectory}AndroidManifest.xml`],
silent: true
});
console.log('Replaced: allowBackup="true" ==> allowBackup="false"');
// apktoolでAPKを再ビルド
await spawn('java', ['-jar', apktool.path, 'b', distDirectory, '-o', `${directory}${revisedApkName}`, '-f']);
// zipalignでAPKを最適化
await spawn('zipalign', ['-f', '-v', '4', `${directory}${revisedApkName}`, `${directory}${distApkName}`]);
// apksignerでAPKに署名
await spawn('apksigner', ['sign', '--ks', keystorePath, '-v', '--v2-signing-enabled', 'true', '--ks-key-alias', keyAlias, '--ks-pass', `pass:${keyPass}`, `${directory}${distApkName}`]);
// 署名を確認
await spawn('apksigner', ['verify', '--print-certs', '-v', `${directory}${distApkName}`]);
})();
Expoでは署名も勝手にやってくれますが、再ビルドした後はkeystoreを用意し署名したりを自分でやらないといけません。
すでにあるものや、作成するなどして任意のkeystoreとエイリアスを使うようにコードは修正してください。
スクリプトの処理は
- apktoolでAPKを展開
- 展開されたファイルを編集
- apktoolでAPKを再ビルド
- zipalignでAPKを最適化
- apksignerでAPKに署名
という流れになっています。
$ node editAndroidManifestXML.js
で実行すると、dist/app
ディレクトリにAPKが展開され、その中のAndroidManifest.xmlが書き換わります。
allowBackup="false"
の挙動を確認
全て問題なく処理が終わり、dist/app-release.apk
が生成されたら、これを確認してみます。
先ほどAndroid端末にインストールした元のアプリをアンインストールし、修正後のアプリをインストールします。
その後、バックアップを実行してみると、
$ adb shell bmgr backupnow <パッケージ名>
Running incremental backup for 1 requested packages.
Package <パッケージ名> with result: Backup is not allowed
Backup finished with result: Success
Backup is not allowed
と表示され、allowBackup="false"
の設定変更が効いているのが確認できました。