Slide 1

Slide 1 text

DevOps on Android From one git push to a Play Store release @JeremMartinez www.jeremie-martinez.com

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Processes depend on context

Slide 5

Slide 5 text

DevOps n.pr [dɛvops] Promotes a release process which bring together through communication and collaboration both development and operational teams. Play Store

Slide 6

Slide 6 text

Continuous integration Automated build Fail fast Constantly tested Constant packaging Easier release

Slide 7

Slide 7 text

Source of a solid process

Slide 8

Slide 8 text

Packaging Quality Tests Build Push

Slide 9

Slide 9 text

Push dev master v21 v22

Slide 10

Slide 10 text

Push feature dev master v21 v22

Slide 11

Slide 11 text

Push feature dev master v21 v22

Slide 12

Slide 12 text

Merge request mandatory At least one reviewer Merge always by the reviewer Branch must always be rebased Push

Slide 13

Slide 13 text

Build Push Packaging Quality Tests

Slide 14

Slide 14 text

Build 2 builds: debug and release

Slide 15

Slide 15 text

Build

Slide 16

Slide 16 text

Build Isolated builds Different architectures possible Empower developers

Slide 17

Slide 17 text

Packaging Quality Tests Build Push

Slide 18

Slide 18 text

Tests Yes, even on Android

Slide 19

Slide 19 text

JUnit4 public final class LordOfTheRingsTest {
 
 @Before
 public void setup() { … }
 
 @Test
 public void shouldBringThePrecious() { … } @After
 public void tearDown() { … } } Unit tests

Slide 20

Slide 20 text

Robolectric public final class LordOfTheRingsTest {
 
 @Before
 public void setup() { … }
 
 @Test
 public void shouldBringThePrecious() { … } @After
 public void tearDown() { … } } Unit tests

Slide 21

Slide 21 text

@RunWith(RobolectricTestRunner.class)
 @Config(manifest = Config.NONE) Unit tests Robolectric public final class LordOfTheRingsTest {
 
 @Before
 public void setup() { … }
 
 @Test
 public void shouldBringThePrecious() { … } @After
 public void tearDown() { … } }

Slide 22

Slide 22 text

Integration tests Espresso or Robotium

Slide 23

Slide 23 text

Integration tests Espresso or Robotium

Slide 24

Slide 24 text

Integration tests Only test main user-flows

Slide 25

Slide 25 text

Integration tests Search Cart Payment Tickets Example for Captain Train

Slide 26

Slide 26 text

Tests

Slide 27

Slide 27 text

Quality Tests Build Push Packaging

Slide 28

Slide 28 text

Quality

Slide 29

Slide 29 text

Quality Lint

Slide 30

Slide 30 text

Quality Lint Custom

Slide 31

Slide 31 text

Quality public class AttrPrefixDetector extends ResourceXmlDetector {
 
 public static final Issue ISSUE = Issue.create("AttrNotPrefixed", //
 "You must prefix your custom attr by `ct`", //
 "We prefix all our attrs to avoid clashes.", //
 Category.TYPOGRAPHY, //
 5, // Priority
 Severity.WARNING, //
 new Implementation(AttrPrefixDetector.class, // Scope.RESOURCE_FILE_SCOPE) // );

Slide 32

Slide 32 text

Quality // Only XML files
 @Override
 public boolean appliesTo(@NonNull Context context,
 @NonNull File file) {
 return LintUtils.isXmlFile(file);
 }

Slide 33

Slide 33 text

Quality // Only values folder
 @Override
 public boolean appliesTo(ResourceFolderType folderType) {
 return ResourceFolderType.VALUES == folderType;
 }

Slide 34

Slide 34 text

Quality // Only attr tag
 @Override
 public Collection getApplicableElements() {
 return Collections.singletonList(TAG_ATTR);
 }

Slide 35

Slide 35 text

Quality // Only name attribute
 @Override
 public Collection getApplicableAttributes() {
 return Collections.singletonList(ATTR_NAME);
 }

Slide 36

Slide 36 text

Quality @Override
 public void visitElement(XmlContext context, Element element) {
 final Attr attributeNode = element.getAttributeNode(ATTR_NAME);
 if (attributeNode != null) {
 final String val = attributeNode.getValue();
 if (!val.startsWith("android:") && !val.startsWith("ct")) {
 context.report(ISSUE,
 attributeNode,
 context.getLocation(attributeNode),
 "You must prefix your custom attr by `ct`");
 }
 }
 }

Slide 37

Slide 37 text

Quality public final class CaptainRegistry extends IssueRegistry {
 @Override
 public List getIssues() {
 return Collections.singletonList(AttrPrefixDetector.ISSUE);
 }
 }

Slide 38

Slide 38 text

Quality

Slide 39

Slide 39 text

Quality Do not underestimate Lint rules

Slide 40

Slide 40 text

Packaging Quality Tests Build Push

Slide 41

Slide 41 text

Packaging Filter your resources Proguard Assemble Sign your APK Zipalign your APK zipalign -z 4 in.apk out.apk

Slide 42

Slide 42 text

Packaging Quality Tests Build Push Continuous integration

Slide 43

Slide 43 text

Packaging Quality Tests Build Push Continuous integration

Slide 44

Slide 44 text

Continuous integration

Slide 45

Slide 45 text

Automate releases in order to deliver quickly, easily and reliably Continuous integration Continuous delivery

Slide 46

Slide 46 text

Continuous deployment

Slide 47

Slide 47 text

« Too many updates »

Slide 48

Slide 48 text

Not enough content

Slide 49

Slide 49 text

Can ≠ Want

Slide 50

Slide 50 text

But … What is a release ?

Slide 51

Slide 51 text

Beta Rollout 5% Rollout 50% Release 100% 1 week 1 week

Slide 52

Slide 52 text

Beta Rollout 5% Rollout 50% Release 100% Every 6 weeks On Tuesday

Slide 53

Slide 53 text

Continuous integration Continuous delivery dev master v21

Slide 54

Slide 54 text

Publish Screenshots Continuous delivery

Slide 55

Slide 55 text

Screenshots Don’t neglect screenshots

Slide 56

Slide 56 text

Screenshots Automate screenshots

Slide 57

Slide 57 text

Home-made with UiAutomator Spoon by Square Screengrab by Fastlane Screenshots

Slide 58

Slide 58 text

Publish Continuous delivery Screenshots

Slide 59

Slide 59 text

Avoid bad surprises Publish

Slide 60

Slide 60 text

Check your permissions Publish

Slide 61

Slide 61 text

private static final String[] EXPECTED_PERMISSIONS = { … } @Test
 public void shouldMatchPermissions() throws Exception {
 final AndroidManifest androidManifest = new AndroidManifest( //
 Fs.fileFromPath("build/intermediates/manifests/full/debug/AndroidManifest.xml"), //
 null, //
 null //
 );
 final Set requestedPermissions = new HashSet<>(androidManifest.getUsedPermissions());
 
 assertThat(requestedPermissions).containsOnly(EXPECTED_PERMISSIONS);
 } Publish

Slide 62

Slide 62 text

Tests database upgrades Publish

Slide 63

Slide 63 text

testCompile 'org.xerial:sqlite-jdbc:3.8.10.1'
 Publish

Slide 64

Slide 64 text

@Test
 public void upgradeShouldBeTheSameAsCreate() throws Exception {
 DbOpenHelper helper = new DbOpenHelper(RuntimeEnvironment.application);
 
 helper.onCreate(newDatabase());
 helper.onUpgrade(originDatabase(), 1, CURRENT_VERSION);
 
 Set newSchema = extractSchema(newFile.getAbsolutePath());
 Set upgradedSchema = extractSchema(upgradedFile.getAbsolutePath());
 
 assertThat(upgradedSchema).isEqualTo(newSchema);
 } Publish

Slide 65

Slide 65 text

connection = DriverManager.getConnection(JDBC_SQLITE + url);
 
 tables = connection.getMetaData().getTables(null, null, null, null);
 while (tables.next()) {
 
 final String tableType = tables.getString("TABLE_TYPE");
 final String tableName = tables.getString("TABLE_NAME");
 schema.add(tableType + " " + tableName);
 Publish

Slide 66

Slide 66 text

columns = connection.getMetaData().getColumns(null, null, tableName, null);
 while (columns.next()) {
 
 final String columnName = columns.getString("COLUMN_NAME");
 final String columnType = columns.getString("TYPE_NAME");
 final String columnNullable = columns.getString("IS_NULLABLE");
 final String columnDefault = columns.getString("COLUMN_DF");
 schema.add("TABLE " + tableName +
 " COLUMN " + columnName + " " + columnType +
 " NULLABLE=" + columnNullable +
 " DEFAULT=" + columnDefault);
 } Publish

Slide 67

Slide 67 text

Tables / Views Columns Primary Keys Cross-references Indexes Publish

Slide 68

Slide 68 text

Publish

Slide 69

Slide 69 text

Publish

Slide 70

Slide 70 text

Publish

Slide 71

Slide 71 text

Publish

Slide 72

Slide 72 text

Publish

Slide 73

Slide 73 text

Publish

Slide 74

Slide 74 text

Choose your client HTTP - Java - Ruby - Python Publish

Slide 75

Slide 75 text

compile 'com.google.apis:google-api-services-androidpublisher:v2-rev20-1.21.0'
 Publish

Slide 76

Slide 76 text

http = GoogleNetHttpTransport.newTrustedTransport();
 json = JacksonFactory.getDefaultInstance();
 
 GoogleCredential credential = new GoogleCredential.Builder().
 setTransport(http).
 setJsonFactory(json).
 setServiceAccountPrivateKeyId(KEY_ID).
 setServiceAccountId(SERVICE_ACCOUNT_EMAIL).
 setServiceAccountScopes(AndroidPublisherScopes.ANDROIDPUBLISHER).
 setServiceAccountPrivateKeyFromPemFile(secretFile).
 build();
 
 publisher = new AndroidPublisher.Builder(http, json, credential).
 setApplicationName(PACKAGE).
 build(); Publish

Slide 77

Slide 77 text

AndroidPublisher.Edits edits = publisher.edits();
 
 AppEdit edit = edits.insert(PACKAGE, null).execute();
 String id = edit.getId(); Publish

Slide 78

Slide 78 text

Listings listings = edits.listings(); Listing listing = new Listing().
 setFullDescription(description).
 setShortDescription(shortDescription).
 setTitle(title);
 
 listings.update(PACKAGE, id, "en_US", listing).execute(); Publish

Slide 79

Slide 79 text

Images images = edits.images();
 
 FileContent content = new FileContent(PNG_MIME_TYPE, file);
 
 images.upload(PACKAGE, id, "en_US", "phone5", content).execute(); Publish "tablet7" "tablet9"
 "wear"

Slide 80

Slide 80 text


 Apks apks = edits.apks(); 
 FileContent apkContent = new FileContent(APK_MIME_TYPE, apkFile);
 Apk apk = apks.upload(PACKAGE, id, apkContent).execute(); 
 int version = apk.getVersionCode();
 Publish

Slide 81

Slide 81 text


 // Assign APK to Track
 Tracks tracks = edits.tracks(); 
 Track track = new Track().setVersionCodes(version); 
 tracks.update(PACKAGE, id, "production", track).execute(); Publish "rollout" "beta" "alpha"

Slide 82

Slide 82 text


 // Update APK listing
 Apklistings apklistings = edits.apklistings(); 
 ApkListing whatsnew = new ApkListing().setRecentChanges(changes); 
 apklistings.update(PACKAGE, id, version, "en_US", whatsnew).execute(); Publish

Slide 83

Slide 83 text

edits.validate(PACKAGE, id).execute(); 
 edits.commit(PACKAGE, id).execute(); Publish

Slide 84

Slide 84 text

Listings Screenshots APK What’s new Track Publish

Slide 85

Slide 85 text

Home-made Gradle plugin Jenkins plugin Publish

Slide 86

Slide 86 text

Screenshots Continuous delivery Publish

Slide 87

Slide 87 text

Publish Screenshots Continuous delivery Packaging Quality Tests Build Push Continuous integration

Slide 88

Slide 88 text

Questions ? @JeremMartinez www.jeremie-martinez.com