LoginSignup
2
2

More than 3 years have passed since last update.

ExpoでAPKをビルド後にAndroidManifest.xmlをいじってみる

Last updated at Posted at 2020-03-22

やりたいこと

ExpoのデフォルトのAndroidManifest.xmlではapplicationのandroid:allowBackuptrueで、つまり自動バックアップが有効になっています。

[追記(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で保存したデータなども含むはず。

App.js
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としてプロジェクトディレクトリに移動するような感じを想定しています。

editAndroidManifestXML.js
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とエイリアスを使うようにコードは修正してください。
スクリプトの処理は

  1. apktoolでAPKを展開
  2. 展開されたファイルを編集
  3. apktoolでAPKを再ビルド
  4. zipalignでAPKを最適化
  5. 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"の設定変更が効いているのが確認できました。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2