WIP
diff --git a/android_connection/apply_to_android.py b/android_connection/apply_to_android.py
index 8eff732..ba75dc1 100644
--- a/android_connection/apply_to_android.py
+++ b/android_connection/apply_to_android.py
@@ -1,13 +1,30 @@
+from absl import app
+from absl import flags
+import filecmp
+from lxml import etree
 from pathlib import Path
-import sys
+import shutil
+
+
+FLAGS = flags.FLAGS
+
+
+flags.DEFINE_string("android_root", None, "Root of android repo")
+flags.DEFINE_bool("dry_run", True, "If False actually update files")
 
 
 def _require_exists(a_file: Path) -> Path:
   if not a_file.is_file():
-    raise ValueError(f"{a_file} not found")
+    raise ValueError(f"{a_file} missing or not a file")
   return a_file
 
 
+def _require_dir_exists(a_dir: Path) -> Path:
+  if not a_dir.is_dir():
+    raise ValueError(f"{a_dir} missing or not a dir")
+  return a_dir
+
+
 def _fonts_xml(android_dir: Path) -> Path:
   return _require_exists(android_dir / "frameworks" / "base" / "data" / "fonts" / "fonts.xml")
 
@@ -16,18 +33,106 @@
   return _require_exists(android_dir / "external" / "noto-fonts" / "fonts.mk")
 
 
+def _font_dir(android_dir: Path) -> Path:
+  return _require_dir_exists(android_dir / "external" / "noto-fonts" / "other")
+
+
+def _repo_root() -> Path:
+  root = (Path(__file__).parent.parent).absolute()
+  if not (root / "LICENSE").is_file():
+    raise IOError(f"{root} does not contain LICENSE")
+  return root
+
+
+def noto_4_android_path() -> Path:
+  xml_file = _repo_root() / "android_connection" / "noto-fonts-4-android.xml"
+  if not xml_file.is_file():
+    raise IOError(f"No file {xml_file}")
+  return xml_file
+
+
+def font_file(font_el) -> str:
+  return ("".join(font_el.itertext())).strip()
+
+
+def font_path(font_el) -> Path:
+  name = font_file(font_el)
+  path = font_el.attrib["path"]
+  return _require_exists(_repo_root() / path / name)
+
+
 def _validate_android_path(android_dir: Path):
   assert android_dir.is_dir(), f"{android_dir} should be a directory"
-  _fonts_xml(android_dir)  # just to trigger it's checks
-  _fonts_mk(android_dir)  # just to trigger it's checks
+    # just to trigger existance validation
+  _fonts_xml(android_dir)
+  _fonts_mk(android_dir)
+  _font_dir(android_dir)
 
 
-def main():
-  if len(sys.argv) != 2:
-    raise ValueError("Must have one arg, path to an Android checkout")
-  android_dir = Path(sys.argv[1])
+def main(_):
+  if not FLAGS.android_root:
+    raise ValueError("Must provide --android_root")
+  android_dir = Path(FLAGS.android_root)
   _validate_android_path(android_dir)
 
+  # gather fonts that should be copied to Android
+  noto_for_android = etree.parse(str(noto_4_android_path()))
+  new_paths = {}
+  for font_el in noto_for_android.xpath("//font[@path]"):
+    path = font_path(font_el)
+    if new_paths.get(path.name, path) != path:
+      raise IOError(f"Multiple paths for {path.name}")
+    new_paths[path.name] = path
+  old_paths = {p.name: p for p in _font_dir(android_dir).glob("Noto*.[ot]t[fc]")}
+
+  new_names = set(new_paths.keys())
+  old_names = set(old_paths.keys())
+
+  delta_sz = 0
+
+  deleted_files = old_names - new_names
+  print(f"{len(deleted_files)} DELETED")
+  for delete_me in sorted(deleted_files):
+    print(f"  {delete_me}")
+    if not FLAGS.dry_run:
+      old_paths[delete_me].unlink()
+  del delete_me
+  del deleted_files
+  print()
+
+  added_files = new_names - old_names
+  print(f"{len(added_files)} ADDED")
+  for add_me in sorted(added_files):
+    dest = _font_dir(android_dir) / add_me
+    print(f"  {add_me}")
+    if not FLAGS.dry_run:
+      shutil.copy(new_paths[add_me], dest)
+  del add_me
+  del added_files
+  print()
+
+  updated_files = new_names & old_names
+  untouched = 0
+  print(f"{len(updated_files)} UPDATED")
+  for update_me in sorted(updated_files):
+    if filecmp.cmp(new_paths[update_me], old_paths[update_me], shallow=False):
+      untouched += 1
+      continue
+
+    dest = _font_dir(android_dir) / update_me
+    print(f"  {update_me}")
+    if not FLAGS.dry_run:
+      shutil.copy(new_paths[update_me], dest)
+  del update_me
+  del updated_files
+  print()
+
+  print(f"{untouched} did not change")
+  print()
+
+  print(f"Done updating files, you should manually update {_fonts_xml(android_dir)}"
+        f" from {noto_4_android_path()}")
+
 
 if __name__ == "__main__":
-  main()
\ No newline at end of file
+    app.run(main)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 982dcb7..351a20f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+absl-py>=0.9.0
 fonttools>=4.28.5
 lxml>=4.7.1
 pytest>=6.2.5
diff --git a/tests/apply_to_android_test.py b/tests/apply_to_android_test.py
index f7d7d88..1ba652d 100644
--- a/tests/apply_to_android_test.py
+++ b/tests/apply_to_android_test.py
@@ -1,4 +1,5 @@
-from android_connection import apply_to_android
+from android_connection.apply_to_android import _validate_android_path
+from lxml import etree
 from pathlib import Path
 import pytest
 
@@ -11,10 +12,11 @@
   return _testdata_dir() / "fake_android"
 
 
-def test_apply_to_bad_dir():
-  with pytest.raises(ValueError, match="not found"):
-    apply_to_android._validate_android_path(_testdata_dir())
+def test_validate_bad_dir():
+  with pytest.raises(ValueError, match="missing"):
+    _validate_android_path(_testdata_dir())
 
 
-def test_apply_to_good_dir():
-  apply_to_android._validate_android_path(_fake_android_dir())
+def test_validate_good_dir():
+  _validate_android_path(_fake_android_dir())
+
diff --git a/tests/noto_fonts_for_android_test.py b/tests/noto_fonts_for_android_test.py
index 2a1bacd..6cb34a9 100644
--- a/tests/noto_fonts_for_android_test.py
+++ b/tests/noto_fonts_for_android_test.py
@@ -4,6 +4,11 @@
 from pathlib import Path
 import pytest
 from typing import Tuple
+from android_connection.apply_to_android import (
+  font_file,
+  font_path,
+  noto_4_android_path,
+)
 
 
 _KNOWN_PATHLESS = {
@@ -14,37 +19,12 @@
 }
 
 
-
-def _repo_root() -> Path:
-  root = (Path(__file__).parent / "..").absolute()
-  if not (root / "LICENSE").is_file():
-    raise IOError(f"{root} does not contain LICENSE")
-  return root
-
-
-def _noto_4_android_file() -> Path:
-  xml_file = _repo_root() / "android_connection" / "noto-fonts-4-android.xml"
-  if not xml_file.is_file():
-    raise IOError(f"No file {xml_file}")
-  return xml_file
-
-
-def _font_file(font_el) -> str:
-  return ("".join(font_el.itertext())).strip()
-
-
-def _font_path(font_el) -> Path:
-  name = _font_file(font_el)
-  path = font_el.attrib["path"]
-  return _repo_root() / path / name
-
-
 def _is_collection(font_el) -> bool:
-  return _font_file(font_el).lower().endswith(".ttc")
+  return font_file(font_el).lower().endswith(".ttc")
 
 
 def _open_font(font_el) -> ttLib.TTFont:
-  path = _font_path(font_el)
+  path = font_path(font_el)
   if not path.is_file():
     raise IOError(f"No such file: {path}")
 
@@ -80,35 +60,35 @@
 
 
 def test_fonts_have_path():
-  root = etree.parse(str(_noto_4_android_file()))
+  root = etree.parse(str(noto_4_android_path()))
   bad = []
   for font in root.iter("font"):
-    font_file = _font_file(font)
-    if font_file in _KNOWN_PATHLESS:
-      assert "path" not in font.attrib, f"{font_file} not expected to have path. Correct _KNOWN_PATHLESS if you just added path"
+    name = font_file(font)
+    if name in _KNOWN_PATHLESS:
+      assert "path" not in font.attrib, f"{name} not expected to have path. Correct _KNOWN_PATHLESS if you just added path"
       continue
 
     if not font.attrib.get("path", ""):
-      bad.append(font_file)
+      bad.append(name)
   assert not bad, "Missing path attribute: " + ", ".join(bad)
 
 
 def test_ttcs_have_index():
-  root = etree.parse(str(_noto_4_android_file()))
+  root = etree.parse(str(noto_4_android_path()))
   bad = []
   for font in root.iter("font"):
     if not _is_collection(font):
       continue
     if "index" not in font.attrib:
-      bad.append(_font_file(font))
+      bad.append(font_file(font))
   assert not bad, "Missing index attribute: " + ", ".join(bad)
 
 
 def test_font_paths_are_valid():
-  root = etree.parse(str(_noto_4_android_file()))
+  root = etree.parse(str(noto_4_android_path()))
   bad = []
   for font in root.xpath("//font[@path]"):
-    path = _font_path(font)
+    path = font_path(font)
     if not path.is_file():
       bad.append(str(path))
   assert not bad, "No such file: " + ", ".join(bad)
@@ -120,17 +100,17 @@
     "NotoNastaliqUrdu-Bold.ttf weight 700 outside font capability 400..400", 
     "NotoSerifMyanmar-Bold.ttf weight 700 outside font capability 400..400"
   }
-  root = etree.parse(str(_noto_4_android_file()))
+  root = etree.parse(str(noto_4_android_path()))
   errors = []
   for font_el in root.xpath("//font[@path]"):
     xml_weight = int(font_el.attrib["weight"])
-    path = _font_path(font_el)
+    path = font_path(font_el)
 
     font = _open_font(font_el)
     min_wght, default_wght, max_weight = _weight(font)
 
     if xml_weight < min_wght or xml_weight > max_weight:
-      error_str = f"{_font_file(font_el)} weight {xml_weight} outside font capability {min_wght}..{max_weight}"
+      error_str = f"{font_file(font_el)} weight {xml_weight} outside font capability {min_wght}..{max_weight}"
       if error_str not in expected_errors:
         errors.append(error_str)
 
@@ -138,21 +118,23 @@
 
 
 def test_font_full_weight_coverage():
-  root = etree.parse(str(_noto_4_android_file()))
+  root = etree.parse(str(noto_4_android_path()))
   errors = []
   for family in root.iter("family"):
     font_to_xml_weights = collections.defaultdict(set)
     for font in family.xpath("//font[@path]"): 
-      font_to_xml_weights[(_font_path(font), font.attrib.get("index", -1))].add(int(font.attrib["weight"]))
+      path = font_path(font)
+      ttc_idx = font.attrib.get("index", -1)
+      font_to_xml_weights[(path, ttc_idx)].add(int(font.attrib["weight"]))
 
   # now you have a map of font path => set of weights in xml
-  for (font_path, font_number), xml_weights in font_to_xml_weights.items():
+  for (path, ttc_idx), xml_weights in font_to_xml_weights.items():
   # open the font, compute the 100 weights between it's min/max weight
   # if xml_weights != computed weights add this to the error list
-    font = _open_font_path(font_path, font_number)
+    font = _open_font_path(path, ttc_idx)
     min_wght, default_wght, max_weight = _weight(font)
     if min(xml_weights) > min_wght or max(xml_weights) < max_weight:
-      errors.append(f"{font_path} weight range {min(xml_weights)}..{max(xml_weights)} could be expanded to {min_wght}..{max_weight}")
+      errors.append(f"{path} weight range {min(xml_weights)}..{max(xml_weights)} could be expanded to {min_wght}..{max_weight}")
 
   assert not errors, ", ".join(errors)
 
diff --git a/tests/testdata/fake_android/external/noto-fonts/other/NotoExisting.ttf b/tests/testdata/fake_android/external/noto-fonts/other/NotoExisting.ttf
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/testdata/fake_android/external/noto-fonts/other/NotoExisting.ttf