Debug Builds: A New Hope (Droidcon MTL 2015)

Debug Builds: A New Hope (Droidcon MTL 2015)

The Cash team at Square moves fast. Really fast. Learn about the tools that we use to eliminate external dependencies and move as fast as possible.

In this talk, I’ll show you how to:
• Quickly verify all of the states of your app by eliminating the server.
• Build and test a server-supported feature before the server is ready.
• Add shortcuts to accelerate your edit-verify cycle.
• Make it easy to submit bug reports with screenshots & full context.

Eefb68011178f8d4e7ae59d1d8f0b0b5?s=128

Matthew Precious

April 10, 2015
Tweet

Transcript

  1. Debug Builds A NEW HOPE Matt Precious

  2. Sample code: U+2020 http://github.com/JakeWharton/u2020

  3. What we’ll need

  4. What we’ll need • Gradle build variants

  5. What we’ll need • Gradle build variants • Dagger

  6. What we’ll need • Gradle build variants • Dagger •

    Retrofit
  7. What we’ll need • Gradle build variants • Dagger •

    Retrofit • OkHttp
  8. What we’ll need • Gradle build variants • Dagger •

    Retrofit • OkHttp • Timber
  9. Build configuration

  10. Build configuration • Types • debug • release

  11. Build configuration • Types • debug • release • Flavors

    • internal • production
  12. Build configuration • Types • debug • release • Flavors

    • internal • production • Variants
  13. Build configuration • Types • debug • release • Flavors

    • internal • production • Variants • internalDebug
  14. Build configuration • Types • debug • release • Flavors

    • internal • production • Variants • internalDebug • internalRelease
  15. Build configuration • Types • debug • release • Flavors

    • internal • production • Variants • internalDebug • internalRelease • productionDebug
  16. Build configuration • Types • debug • release • Flavors

    • internal • production • Variants • internalDebug • internalRelease • productionDebug • productionRelease
  17. Debug drawer

  18. Debug drawer

  19. MainActivity (src/main) public final class MainActivity extends Activity {
 


    @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState); setContentView(R.layout.main_activity);
 }
 }a
  20. Activity Content Container Content

  21. Activity Content Container Content Debug Drawer Debug View

  22. AppContainer (src/main) public interface AppContainer {
 ViewGroup bind(Activity activity);
 }a

  23. AppContainer (src/main) public interface AppContainer {
 ViewGroup bind(Activity activity);
 


    AppContainer DEFAULT = new AppContainer() {
 @Override public ViewGroup bind(Activity activity) {
 return findById(activity, android.R.id.content);
 }
 };
 }a
  24. UiModule (src/main) @Provides @Singleton AppContainer provideAppContainer() {
 return AppContainer.DEFAULT;
 }a

  25. MainActivity (src/main) public final class MainActivity extends Activity {
 


    @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState); setContentView(R.layout.main_activity);
 }a
 }a
  26. MainActivity (src/main) public final class MainActivity extends Activity {
 @Inject

    AppContainer appContainer;
 
 @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 ObjectGraph appGraph = Injector.obtain(getApplication());
 appGraph.inject(this); ! setContentView(R.layout.main_activity);
 }a
 }a
  27. MainActivity (src/main) public final class MainActivity extends Activity {
 @Inject

    AppContainer appContainer;
 
 @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 ObjectGraph appGraph = Injector.obtain(getApplication());
 appGraph.inject(this);
 
 ViewGroup container = appContainer.bind(this); ! setContentView(R.layout.main_activity);
 }a
 }a
  28. MainActivity (src/main) public final class MainActivity extends Activity {
 @Inject

    AppContainer appContainer;
 
 @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 ObjectGraph appGraph = Injector.obtain(getApplication());
 appGraph.inject(this);
 
 ViewGroup container = appContainer.bind(this); ! setContentView(R.layout.main_activity);
 }a
 }a
  29. MainActivity (src/main) public final class MainActivity extends Activity {
 @Inject

    AppContainer appContainer;
 
 @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 ObjectGraph appGraph = Injector.obtain(getApplication());
 appGraph.inject(this);
 
 ViewGroup container = appContainer.bind(this);
 
 getLayoutInflater().inflate(R.layout.main_activity, container);
 }a
 }a
  30. debug_activity_frame (src/internalDebug) <DrawerLayout android:id="@+id/debug_drawer_layout"> 
 <FrameLayout android:id="@+id/debug_content" /> 
 <ScrollView


    android:id="@+id/debug_drawer"
 android:layout_gravity="right" android:background="#ee212121" /> 
 </DrawerLayout>
  31. debug_activity_frame (src/internalDebug) <DrawerLayout android:id="@+id/debug_drawer_layout"> 
 <FrameLayout android:id="@+id/debug_content" /> 
 <ScrollView


    android:id="@+id/debug_drawer"
 android:layout_gravity="right" android:background="#ee212121"
 /> 
 </DrawerLayout>
  32. debug_activity_frame (src/internalDebug) <DrawerLayout android:id="@+id/debug_drawer_layout"> 
 <FrameLayout android:id="@+id/debug_content" /> 
 <ScrollView


    android:id="@+id/debug_drawer"
 android:layout_gravity="right" android:background="#ee212121"
 /> 
 </DrawerLayout>
  33. DebugView (src/internalDebug) public final class DebugView extends FrameLayout { !

    public DebugView(Context context) {
 this(context, null);
 }a
 
 public DebugView(Context context, AttributeSet attrs) {
 super(context, attrs);
 Injector.obtain(context).inject(this);
 
 LayoutInflater.from(context).inflate(R.layout.debug_view_content, this); }a }a
  34. DebugView (src/internalDebug) public final class DebugView extends FrameLayout { !

    public DebugView(Context context) {
 this(context, null);
 }a
 
 public DebugView(Context context, AttributeSet attrs) {
 super(context, attrs);
 Injector.obtain(context).inject(this);
 
 LayoutInflater.from(context).inflate(R.layout.debug_view_content, this); }a }a
  35. DebugAppContainer (src/internalDebug) @Singleton
 public final class DebugAppContainer implements AppContainer {

    ! @Inject public DebugAppContainer() { } ! @Override public ViewGroup bind(final Activity activity) { }a }a
  36. DebugAppContainer (src/internalDebug) @Singleton
 public final class DebugAppContainer implements AppContainer {

    ! @Inject public DebugAppContainer() { } ! @Override public ViewGroup bind(final Activity activity) { activity.setContentView(R.layout.debug_activity_frame); }a }a
  37. DebugAppContainer (src/internalDebug) @Singleton
 public final class DebugAppContainer implements AppContainer {

    ! @Inject public DebugAppContainer() { } ! @Override public ViewGroup bind(final Activity activity) { activity.setContentView(R.layout.debug_activity_frame); ! ViewGroup drawer = findById(activity, R.id.debug_drawer);
 DebugView debugView = new DebugView(activity);
 drawer.addView(debugView); }a }a
  38. DebugAppContainer (src/internalDebug) @Singleton
 public final class DebugAppContainer implements AppContainer {

    ! @Inject public DebugAppContainer() { } ! @Override public ViewGroup bind(final Activity activity) { activity.setContentView(R.layout.debug_activity_frame); ! ViewGroup drawer = findById(activity, R.id.debug_drawer);
 DebugView debugView = new DebugView(activity);
 drawer.addView(debugView); ! return findById(activity, R.id.debug_content); ! }a }a
  39. DebugUiModule (src/internalDebug) @Provides @Singleton AppContainer provideAppContainer(DebugAppContainer debugAppContainer) {
 return debugAppContainer;


    }a
  40. Endpoints

  41. Endpoints

  42. ApiModule (src/main) public static final String PRODUCTION_API_URL = "https://api.github.com";
 


    @Provides @Singleton Endpoint provideEndpoint() {
 return Endpoints.newFixedEndpoint(PRODUCTION_API_URL);
 }
  43. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 PRODUCTION,
 STAGING,
 CUSTOM,
 }aa

  44. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 PRODUCTION("Production", ApiModule.PRODUCTION_API_URL),
 STAGING("Staging", "https://api.staging.github.com/"),


    CUSTOM("Custom", null);
 
 public final String name;
 public final String url;
 
 ApiEndpoints(String name, String url) {
 this.name = name;
 this.url = url;
 }a
 }aa
  45. DebugDataModule (src/internalDebug) @Provides @Singleton @ApiEndpoint
 StringPreference provideEndpointPreference(SharedPreferences preferences) {
 return

    new StringPreference(preferences, “debug_endpoint", ApiEndpoints.STAGING.url);
 }
  46. DebugApiModule (src/internalDebug) @Provides @Singleton
 Endpoint provideEndpoint(@ApiEndpoint StringPreference apiEndpoint) {
 return

    Endpoints.newFixedEndpoint(apiEndpoint.get());
 }
  47. debug_view_content (src/internalDebug) <TextView style="@style/Widget.U2020.DebugDrawer.RowTitle" android:text="Endpoint"
 /> ! <Spinner android:id="@+id/debug_network_endpoint" />

  48. DebugView (src/internalDebug) private void setupNetworkSection() {
 }a

  49. DebugView (src/internalDebug) @Inject @ApiEndpoint StringPreference networkEndpoint; ! private void setupNetworkSection()

    {
 final ApiEndpoints currentEndpoint = ApiEndpoints.from(networkEndpoint.get());
 }aa
  50. DebugView (src/internalDebug) @InjectView(R.id.debug_network_endpoint) Spinner endpointView; @Inject @ApiEndpoint StringPreference networkEndpoint; !

    private void setupNetworkSection() {
 final ApiEndpoints currentEndpoint = ApiEndpoints.from(networkEndpoint.get());
 final EnumAdapter<ApiEndpoints> endpointAdapter =
 new EnumAdapter<>(getContext(), ApiEndpoints.class);
 endpointView.setAdapter(endpointAdapter);
 }aa
  51. DebugView (src/internalDebug) @InjectView(R.id.debug_network_endpoint) Spinner endpointView; @Inject @ApiEndpoint StringPreference networkEndpoint; !

    private void setupNetworkSection() {
 final ApiEndpoints currentEndpoint = ApiEndpoints.from(networkEndpoint.get());
 final EnumAdapter<ApiEndpoints> endpointAdapter =
 new EnumAdapter<>(getContext(), ApiEndpoints.class);
 endpointView.setAdapter(endpointAdapter);
 }aa
  52. DebugView (src/internalDebug) @InjectView(R.id.debug_network_endpoint) Spinner endpointView; @Inject @ApiEndpoint StringPreference networkEndpoint; !

    private void setupNetworkSection() {
 final ApiEndpoints currentEndpoint = ApiEndpoints.from(networkEndpoint.get());
 final EnumAdapter<ApiEndpoints> endpointAdapter =
 new EnumAdapter<>(getContext(), ApiEndpoints.class);
 endpointView.setAdapter(endpointAdapter);
 endpointView.setSelection(currentEndpoint.ordinal());
 }aa
  53. DebugView (src/internalDebug) @InjectView(R.id.debug_network_endpoint) Spinner endpointView; @Inject @ApiEndpoint StringPreference networkEndpoint; !

    private void setupNetworkSection() {
 // ...
 endpointView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 ApiEndpoints selected = endpointAdapter.getItem(position);
 if (selected != currentEndpoint) {
 if (selected == ApiEndpoints.CUSTOM) {
 showCustomEndpointDialog();
 } else {
 setEndpointAndRelaunch(selected.url);
 }a
 }a
 });
 }aa
  54. DebugView (src/internalDebug) @InjectView(R.id.debug_network_endpoint) Spinner endpointView; @Inject @ApiEndpoint StringPreference networkEndpoint; !

    private void setupNetworkSection() {
 // ...
 endpointView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 ApiEndpoints selected = endpointAdapter.getItem(position);
 if (selected != currentEndpoint) {
 if (selected == ApiEndpoints.CUSTOM) {
 showCustomEndpointDialog();
 } else {
 setEndpointAndRelaunch(selected.url);
 }a
 }a
 });
 }aa
  55. DebugView (src/internalDebug) @InjectView(R.id.debug_network_endpoint) Spinner endpointView; @Inject @ApiEndpoint StringPreference networkEndpoint; !

    private void setupNetworkSection() {
 // ...
 endpointView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 ApiEndpoints selected = endpointAdapter.getItem(position);
 if (selected != currentEndpoint) {
 if (selected == ApiEndpoints.CUSTOM) {
 showCustomEndpointDialog();
 } else {
 setEndpointAndRelaunch(selected.url);
 }a
 }a
 });
 }aa
  56. DebugView (src/internalDebug) @Inject @ApiEndpoint StringPreference networkEndpoint; ! private void setupNetworkSection()

    {
 // ...
 }aa ! private void setEndpointAndRelaunch(String endpoint) {
 networkEndpoint.set(endpoint);
 
 ProcessPhoenix.triggerRebirth(getContext());
 }a
  57. DebugView (src/internalDebug) @Inject @ApiEndpoint StringPreference networkEndpoint; ! private void setupNetworkSection()

    {
 // ...
 }aa ! private void setEndpointAndRelaunch(String endpoint) {
 networkEndpoint.set(endpoint);
 
 ProcessPhoenix.triggerRebirth(getContext());
 }a ! ! ! https://gist.github.com/JakeWharton
  58. Charles

  59. Charles • HTTP proxy

  60. Charles • HTTP proxy • Monitor network traffic

  61. Charles • HTTP proxy • Monitor network traffic • Request

  62. Charles • HTTP proxy • Monitor network traffic • Request

    • Response
  63. Charles • HTTP proxy • Monitor network traffic • Request

    • Response • Headers
  64. Charles • HTTP proxy • Monitor network traffic • Request

    • Response • Headers • Intercept and modify requests and responses
  65. Charles • HTTP proxy • Monitor network traffic • Request

    • Response • Headers • Intercept and modify requests and responses • http://www.charlesproxy.com/
  66. Network Proxy

  67. Network Proxy

  68. Network Proxy

  69. DataModule (src/main) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app) {
 return createOkHttpClient(app);


    } ! static OkHttpClient createOkHttpClient(Application app) {
 OkHttpClient client = new OkHttpClient();
 // ... 
 return client;
 }
  70. NetworkProxyPreference (src/internalDebug) public final class NetworkProxyPreference extends StringPreference {
 public

    NetworkProxyPreference(SharedPreferences preferences, String key) {
 super(preferences, key);
 }
 
 public @Nullable Proxy getProxy() {
 if (!isSet()) return null;
 
 String[] parts = get().split(":", 2);
 String host = parts[0];
 int port = parts.length > 1 ? Integer.parseInt(parts[1]) : 80;
 
 return new Proxy(HTTP, InetSocketAddress.createUnresolved(host, port));
 }
 }
  71. DebugDataModule (src/internalDebug) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app) {
 OkHttpClient client

    = DataModule.createOkHttpClient(app);
 return client;
 }a
  72. DebugDataModule (src/internalDebug) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app,
 NetworkProxyPreference networkProxy) {


    OkHttpClient client = DataModule.createOkHttpClient(app); 
 client.setProxy(networkProxy.getProxy());
 return client;
 }a
  73. DebugDataModule (src/internalDebug) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app,
 NetworkProxyPreference networkProxy) {


    OkHttpClient client = DataModule.createOkHttpClient(app); 
 client.setProxy(networkProxy.getProxy()); client.setSslSocketFactory(createBadSslSocketFactory());
 return client;
 }a
  74. debug_view_content (src/internalDebug) <TextView style="@style/Widget.U2020.DebugDrawer.RowTitle"
 android:text="Proxy"
 /> 
 <Spinner android:id="@+id/debug_network_proxy" />

  75. DebugView (src/internalDebug) private void setupNetworkSection() { // ... }aa

  76. DebugView (src/internalDebug) @InjectView(R.id.debug_network_proxy) Spinner networkProxyView; @Inject NetworkProxyPreference networkProxy; ! private

    void setupNetworkSection() { // ... 
 final ProxyAdapter proxyAdapter = new ProxyAdapter(getContext(), networkProxy);
 networkProxyView.setAdapter(proxyAdapter);
 }aa
  77. DebugView (src/internalDebug) @InjectView(R.id.debug_network_proxy) Spinner networkProxyView; @Inject NetworkProxyPreference networkProxy; ! private

    void setupNetworkSection() { // ... 
 final ProxyAdapter proxyAdapter = new ProxyAdapter(getContext(), networkProxy);
 networkProxyView.setAdapter(proxyAdapter); int proxyPosition = networkProxy.isSet() ? ProxyAdapter.PROXY : ProxyAdapter.NONE;
 networkProxyView.setSelection(proxyPosition);
 }aa
  78. DebugView (src/internalDebug) @InjectView(R.id.debug_network_proxy) Spinner networkProxyView; @Inject NetworkProxyPreference networkProxy; @Inject OkHttpClient

    client; ! private void setupNetworkSection() { // ... 
 networkProxyView.setOnItemSelectedListener((adapterView, view, position, d) -> {
 if (position == ProxyAdapter.NONE) {
 networkProxy.delete();
 client.setProxy(null);
 } else {
 showNewNetworkProxyDialog();
 }a
 }); }aa
  79. DebugView (src/internalDebug) @InjectView(R.id.debug_network_proxy) Spinner networkProxyView; @Inject NetworkProxyPreference networkProxy; @Inject OkHttpClient

    client; ! private void setupNetworkSection() { // ... ! networkProxyView.setOnItemSelectedListener((adapterView, view, position, d) -> {
 if (position == ProxyAdapter.NONE) {
 networkProxy.delete();
 client.setProxy(null);
 } else {
 showNewNetworkProxyDialog();
 }a
 }); }aa
  80. DebugView (src/internalDebug) @InjectView(R.id.debug_network_proxy) Spinner networkProxyView; @Inject NetworkProxyPreference networkProxy; @Inject OkHttpClient

    client; ! private void setupNetworkSection() { // ... 
 networkProxyView.setOnItemSelectedListener((adapterView, view, position, d) -> {
 if (position == ProxyAdapter.NONE) {
 networkProxy.delete();
 client.setProxy(null);
 } else {
 showNewNetworkProxyDialog();
 }a
 }); }aa
  81. DebugView (src/internalDebug) @InjectView(R.id.debug_network_proxy) Spinner networkProxyView; @Inject NetworkProxyPreference networkProxy; @Inject OkHttpClient

    client; ! private void setupNetworkSection() { // ... }aa ! private void setProxyHost(String host) {
 networkProxy.set(host);
 
 Proxy proxy = networkProxy.getProxy();
 client.setProxy(proxy);
 }
  82. Mock Mode

  83. GithubService (src/main) public interface GithubService {
 @GET("/search/repositories") //
 Observable<RepositoriesResponse> repositories(


    @Query("q") SearchQuery query,
 @Query("sort") Sort sort,
 @Query("order") Order order);
 }
  84. ApiModule (src/main) @Provides @Singleton GithubService provideGithubService(RestAdapter restAdapter) {
 return restAdapter.create(GithubService.class);


    }
  85. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 PRODUCTION("Production", ApiModule.PRODUCTION_API_URL),
 STAGING("Staging", "https://api.staging.github.com/"),


    CUSTOM("Custom", null);
 
 public final String name;
 public final String url;
 
 ApiEndpoints(String name, String url) {
 this.name = name;
 this.url = url;
 }a
 }aa
  86. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 PRODUCTION("Production", ApiModule.PRODUCTION_API_URL),
 STAGING("Staging", "https://api.staging.github.com/"),

    MOCK_MODE("Mock mode", "mock://"),
 CUSTOM("Custom", null);
 
 public final String name;
 public final String url;
 
 ApiEndpoints(String name, String url) {
 this.name = name;
 this.url = url;
 }a
 }aa
  87. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 // ...
 public static

    boolean isMockMode(String endpoint) {
 return from(endpoint) == MOCK_MODE;
 }a }aa
  88. DebugDataModule (src/internalDebug) @Provides @Singleton @IsMockMode boolean provideIsMockMode(@ApiEndpoint StringPreference endpoint) {


    return ApiEndpoints.isMockMode(endpoint.get());
 }
  89. MockRepositories (src/internalDebug) static final Repository DAGGER = new Repository.Builder()
 .name("dagger")


    .owner(SQUARE)
 .description("A fast dependency injector for Android and Java.")
 .forks(574)
 .stars(3085)
 .htmlUrl("https://github.com/square/dagger")
 .updatedAt(DateTime.parse("2015-03-05"))
 .build(); ! // ...
  90. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    
 @Inject MockGithubService() { }
 }a
  91. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    
 @Inject MockGithubService() { }
 
 @Override public Observable<RepositoriesResponse> repositories(SearchQuery query, Sort sort, Order order) { return Observable.just(new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,
 DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020)));
 }
 }a
  92. DebugApiModule (src/internalDebug) @Provides @Singleton
 GithubService provideGithubService(RestAdapter restAdapter, MockRestAdapter mockRestAdapter, @IsMockMode

    boolean isMockMode, MockGithubService mockService) {
 if (isMockMode) {
 return mockRestAdapter.create(GithubService.class, mockService);
 } 
 return restAdapter.create(GithubService.class);
 }
  93. None
  94. :)

  95. :) Loading Error Empty One result Lots of results

  96. :) Loading Error Empty One result Lots of results ✔

  97. :) Loading Error Empty One result Lots of results ✔

  98. debug_view_content (src/internalDebug) <TextView style="@style/Widget.U2020.DebugDrawer.RowTitle"
 android:text="Delay"
 />
 <Spinner android:id="@+id/debug_network_delay" />
 


    <TextView style="@style/Widget.U2020.DebugDrawer.RowTitle"
 android:text="Error"
 />
 <Spinner android:id="@+id/debug_network_error" />
  99. DebugApiModule @Provides @Singleton MockRestAdapter provideMockRestAdapter(RestAdapter restAdapter,
 SharedPreferences preferences) {
 MockRestAdapter

    mockRestAdapter = MockRestAdapter.from(restAdapter);
 AndroidMockValuePersistence.install(mockRestAdapter, preferences);
 return mockRestAdapter;
 }
  100. DebugView (src/internalDebug) private void setupNetworkSection() { // ... }aa

  101. DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; ! private void setupNetworkSection() {

    // ... ! final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 }aa
  102. DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; @Inject MockRestAdapter mockRestAdapter; ! private

    void setupNetworkSection() { // ... ! final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 networkDelayView.setSelection(
 NetworkDelayAdapter.getPositionForValue(mockRestAdapter.getDelay())); }aa
  103. DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; @Inject MockRestAdapter mockRestAdapter; ! private

    void setupNetworkSection() { // ... ! final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 networkDelayView.setSelection(
 NetworkDelayAdapter.getPositionForValue(mockRestAdapter.getDelay())); }aa
  104. DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; @Inject MockRestAdapter mockRestAdapter; ! private

    void setupNetworkSection() { // ... ! final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 networkDelayView.setSelection(
 NetworkDelayAdapter.getPositionForValue(mockRestAdapter.getDelay()));
 networkDelayView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 int selected = delayAdapter.getItem(position);
 if (selected != mockRestAdapter.getDelay()) {
 mockRestAdapter.setDelay(selected);
 }a
 }); }aa
  105. DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; @Inject MockRestAdapter mockRestAdapter; ! private

    void setupNetworkSection() { // ... ! final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 networkDelayView.setSelection(
 NetworkDelayAdapter.getPositionForValue(mockRestAdapter.getDelay()));
 networkDelayView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 int selected = delayAdapter.getItem(position);
 if (selected != mockRestAdapter.getDelay()) {
 mockRestAdapter.setDelay(selected);
 }a
 }); }aa
  106. DebugView (src/internalDebug) @InjectView(R.id.debug_network_error) Spinner networkErrorView; @Inject MockRestAdapter mockRestAdapter; ! private

    void setupNetworkSection() { // ... ! final NetworkErrorAdapter errorAdapter = new NetworkErrorAdapter(getContext());
 networkErrorView.setAdapter(errorAdapter);
 networkErrorView.setSelection(
 NetworkErrorAdapter.getPositionForValue(mockRestAdapter.getErrorPercentage()));
 networkErrorView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 int selected = errorAdapter.getItem(position);
 if (selected != mockRestAdapter.getErrorPercentage()) {
 mockRestAdapter.setErrorPercentage(selected);
 }a
 }); }aa
  107. DebugView (src/internalDebug) @InjectView(R.id.debug_network_error) Spinner networkErrorView; @Inject MockRestAdapter mockRestAdapter; ! private

    void setupNetworkSection() { // ... ! final NetworkErrorAdapter errorAdapter = new NetworkErrorAdapter(getContext());
 networkErrorView.setAdapter(errorAdapter);
 networkErrorView.setSelection(
 NetworkErrorAdapter.getPositionForValue(mockRestAdapter.getErrorPercentage()));
 networkErrorView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 int selected = errorAdapter.getItem(position);
 if (selected != mockRestAdapter.getErrorPercentage()) {
 mockRestAdapter.setErrorPercentage(selected);
 }a
 }); }aa
  108. :) Loading Error Empty One result Lots of results ✔

  109. :) Loading Error Empty One result Lots of results ✔

    ✔ ✔
  110. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    
 @Inject MockGithubService() {
 }a
 
 @Override public Observable<RepositoriesResponse> repositories(SearchQuery query, Sort sort, Order order) { return Observable.just(new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,
 DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020)));
 }a
 }aa
  111. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    
 @Inject MockGithubService() {
 }a
 
 @Override public Observable<RepositoriesResponse> repositories(SearchQuery query, Sort sort, Order order) { return Observable.just(new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,
 DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020)));
 }a
 }aa
  112. new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,
 DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020))

  113. MockRepositoriesResponse (src/internalDebug) public enum MockRepositoriesResponse {
 SUCCESS("Success", new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,


    DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020)));
 
 public final String name;
 public final RepositoriesResponse response;
 
 MockRepositoriesResponse(String name, RepositoriesResponse response) {
 this.name = name;
 this.response = response;
 }a
 }aa
  114. MockRepositoriesResponse (src/internalDebug) public enum MockRepositoriesResponse {
 SUCCESS("Success", new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,


    DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020))), ONE("One", new RepositoriesResponse(Collections.singletonList(DAGGER))),
 EMPTY("Empty", new RepositoriesResponse(null));
 
 public final String name;
 public final RepositoriesResponse response;
 
 MockRepositoriesResponse(String name, RepositoriesResponse response) {
 this.name = name;
 this.response = response;
 }a
 }aa
  115. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    // ...
 }aa
  116. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    // ...
 
 public <T extends Enum<T>> T getResponse(Class<T> responseClass) { // } 
 public <T extends Enum<T>> void setResponse(Class<T> responseClass, T value) { // }
 }aa
  117. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    // ...
 
 public <T extends Enum<T>> T getResponse(Class<T> responseClass) { // } 
 public <T extends Enum<T>> void setResponse(Class<T> responseClass, T value) { // }
 
 @Override public Observable<RepositoriesResponse> repositories(SearchQuery query,
 Sort sort, Order order) {
 }a
 }aa
  118. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    // ...
 
 public <T extends Enum<T>> T getResponse(Class<T> responseClass) { // } 
 public <T extends Enum<T>> void setResponse(Class<T> responseClass, T value) { // }
 
 @Override public Observable<RepositoriesResponse> repositories(SearchQuery query,
 Sort sort, Order order) {
 RepositoriesResponse response = getResponse(MockRepositoriesResponse.class).response;
 }a
 }aa
  119. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    // ...
 
 public <T extends Enum<T>> T getResponse(Class<T> responseClass) { // } 
 public <T extends Enum<T>> void setResponse(Class<T> responseClass, T value) { // }
 
 @Override public Observable<RepositoriesResponse> repositories(SearchQuery query,
 Sort sort, Order order) {
 RepositoriesResponse response = getResponse(MockRepositoriesResponse.class).response;
 
 // TODO: Sort the repositories based on the request.
 }a
 }aa
  120. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    // ...
 
 public <T extends Enum<T>> T getResponse(Class<T> responseClass) { // } 
 public <T extends Enum<T>> void setResponse(Class<T> responseClass, T value) { // }
 
 @Override public Observable<RepositoriesResponse> repositories(SearchQuery query,
 Sort sort, Order order) {
 RepositoriesResponse response = getResponse(MockRepositoriesResponse.class).response;
 // TODO: Sort the repositories based on the request.
 
 return Observable.just(response);
 }a
 }aa
  121. debug_view_content (src/internalDebug) <TextView style="@style/Widget.U2020.DebugDrawer.RowTitle"
 android:text="Repositories"
 /> 
 <Spinner android:id="@+id/debug_repositories_response" />

  122. DebugView (src/internalDebug) private <T extends Enum<T>> void configureResponseSpinner(Spinner spinner,
 final

    Class<T> responseClass) {
 }aa
  123. DebugView (src/internalDebug) private <T extends Enum<T>> void configureResponseSpinner(Spinner spinner,
 final

    Class<T> responseClass) {
 }aa
  124. DebugView (src/internalDebug) private <T extends Enum<T>> void configureResponseSpinner(Spinner spinner,
 final

    Class<T> responseClass) {
 }aa
  125. DebugView (src/internalDebug) private <T extends Enum<T>> void configureResponseSpinner(Spinner spinner,
 final

    Class<T> responseClass) {
 final EnumAdapter<T> adapter = new EnumAdapter<>(getContext(), responseClass); spinner.setAdapter(adapter); }aa
  126. DebugView (src/internalDebug) @Inject @IsMockMode boolean isMockMode; ! private <T extends

    Enum<T>> void configureResponseSpinner(Spinner spinner,
 final Class<T> responseClass) {
 final EnumAdapter<T> adapter = new EnumAdapter<>(getContext(), responseClass); spinner.setAdapter(adapter);
 spinner.setEnabled(isMockMode); }aa
  127. DebugView (src/internalDebug) @Inject @IsMockMode boolean isMockMode; @Inject MockGithubService mockGithubService; !

    private <T extends Enum<T>> void configureResponseSpinner(Spinner spinner,
 final Class<T> responseClass) {
 final EnumAdapter<T> adapter = new EnumAdapter<>(getContext(), responseClass); spinner.setAdapter(adapter);
 spinner.setEnabled(isMockMode);
 spinner.setSelection(mockGithubService.getResponse(responseClass).ordinal());
 }aa
  128. DebugView (src/internalDebug) @Inject @IsMockMode boolean isMockMode; @Inject MockGithubService mockGithubService; !

    private <T extends Enum<T>> void configureResponseSpinner(Spinner spinner,
 final Class<T> responseClass) {
 final EnumAdapter<T> adapter = new EnumAdapter<>(getContext(), responseClass); spinner.setAdapter(adapter);
 spinner.setEnabled(isMockMode);
 spinner.setSelection(mockGithubService.getResponse(responseClass).ordinal());
 spinner.setOnItemSelectedListener((parent, view, position, id) -> {
 T selected = adapter.getItem(position);
 if (selected != mockGithubService.getResponse(responseClass)) {
 mockGithubService.setResponse(responseClass, selected);
 }a
 });
 }aa
  129. DebugView (src/internalDebug) @InjectView(R.id.debug_repositories_response) Spinner repositoriesResponseView; ! private void setupMockBehaviorSection() {


    configureResponseSpinner(repositoriesResponseView, MockRepositoriesResponse.class);
 } ! private <T extends Enum<T>> void configureResponseSpinner(Spinner spinner,
 final Class<T> responseClass) {
 // ...
 }aa
  130. Intent Capturing

  131. Intent Capturing

  132. IntentFactory (src/main) public interface IntentFactory {
 }a

  133. IntentFactory (src/main) public interface IntentFactory {
 Intent createUrlIntent(String url);
 }a

  134. IntentFactory (src/main) public interface IntentFactory {
 Intent createUrlIntent(String url);
 


    IntentFactory REAL = new IntentFactory() {
 @Override public Intent createUrlIntent(String url) {
 Intent intent = new Intent(Intent.ACTION_VIEW);
 intent.setData(Uri.parse(url));
 
 return intent;
 }
 };
 }a
  135. DataModule (src/main) @Provides @Singleton IntentFactory provideIntentFactory() {
 return IntentFactory.REAL;
 }

  136. ExternalIntentActivity (src/internalDebug) public final class ExternalIntentActivity extends Activity {
 }a

  137. ExternalIntentActivity (src/internalDebug) public final class ExternalIntentActivity extends Activity {
 public

    static final String ACTION = "com.jakewharton.u2020.intent.EXTERNAL_INTENT";
 public static final String EXTRA_BASE_INTENT = "debug_base_intent";
 }a
  138. ExternalIntentActivity (src/internalDebug) public final class ExternalIntentActivity extends Activity {
 public

    static final String ACTION = "com.jakewharton.u2020.intent.EXTERNAL_INTENT";
 public static final String EXTRA_BASE_INTENT = "debug_base_intent"; ! public static Intent createIntent(Intent baseIntent) {
 Intent intent = new Intent();
 intent.setAction(ACTION);
 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 intent.putExtra(EXTRA_BASE_INTENT, baseIntent);
 return intent;
 }b
 }a
  139. ExternalIntentActivity (src/internalDebug) public final class ExternalIntentActivity extends Activity {
 public

    static final String ACTION = "com.jakewharton.u2020.intent.EXTERNAL_INTENT";
 public static final String EXTRA_BASE_INTENT = "debug_base_intent"; ! public static Intent createIntent(Intent baseIntent) {
 Intent intent = new Intent();
 intent.setAction(ACTION);
 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 intent.putExtra(EXTRA_BASE_INTENT, baseIntent);
 return intent;
 }b
 }a
  140. ExternalIntentActivity (src/internalDebug) public final class ExternalIntentActivity extends Activity {
 public

    static final String ACTION = "com.jakewharton.u2020.intent.EXTERNAL_INTENT";
 public static final String EXTRA_BASE_INTENT = "debug_base_intent";
 // ... 
 @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.debug_external_intent_activity);
 
 Intent baseIntent = getIntent().getParcelableExtra(EXTRA_BASE_INTENT);
 // TODO: Show intent data.
 }
 }a
  141. DebugDataModule (src/internalDebug) @Provides @Singleton @CaptureIntents
 BooleanPreference provideCaptureIntentsPreference(SharedPreferences preferences) {
 return

    new BooleanPreference(preferences, "debug_capture_intents", DEFAULT_CAPTURE_INTENTS);
 }
  142. DebugIntentFactory (src/internalDebug) @Singleton
 public final class DebugIntentFactory implements IntentFactory {


    
 @Inject public DebugIntentFactory() { }
 }aa
  143. DebugIntentFactory (src/internalDebug) @Singleton
 public final class DebugIntentFactory implements IntentFactory {


    private final IntentFactory realIntentFactory;
 private final boolean isMockMode;
 private final BooleanPreference captureIntents;
 
 @Inject public DebugIntentFactory( // ... ) { // ... }
 }aa
  144. DebugIntentFactory (src/internalDebug) @Singleton
 public final class DebugIntentFactory implements IntentFactory {


    private final IntentFactory realIntentFactory;
 private final boolean isMockMode;
 private final BooleanPreference captureIntents;
 
 @Inject public DebugIntentFactory( // ... ) { // ... } ! @Override public Intent createUrlIntent(String url) {
 }a
 }aa
  145. DebugIntentFactory (src/internalDebug) @Singleton
 public final class DebugIntentFactory implements IntentFactory {


    private final IntentFactory realIntentFactory;
 private final boolean isMockMode;
 private final BooleanPreference captureIntents;
 
 @Inject public DebugIntentFactory( // ... ) { // ... } ! @Override public Intent createUrlIntent(String url) {
 Intent baseIntent = realIntentFactory.createUrlIntent(url);
 }a
 }aa
  146. DebugIntentFactory (src/internalDebug) @Singleton
 public final class DebugIntentFactory implements IntentFactory {


    private final IntentFactory realIntentFactory;
 private final boolean isMockMode;
 private final BooleanPreference captureIntents;
 
 @Inject public DebugIntentFactory( // ... ) { // ... } ! @Override public Intent createUrlIntent(String url) {
 Intent baseIntent = realIntentFactory.createUrlIntent(url);
 if (isMockMode && captureIntents.get()) { return ExternalIntentActivity.createIntent(baseIntent);
 } else {
 return baseIntent;
 }
 }a
 }aa
  147. DebugDataModule (src/internalDebug) @Provides @Singleton IntentFactory provideIntentFactory(DebugIntentFactory debugIntentFactory) {
 return debugIntentFactory;


    }
  148. Bug Reporting

  149. Bug Reporting

  150. Bug Reporting

  151. Telescope

  152. Telescope • Press and hold 2 fingers to trigger a

    bug report
  153. Telescope • Press and hold 2 fingers to trigger a

    bug report • Takes a screenshot
  154. Telescope • Press and hold 2 fingers to trigger a

    bug report • Takes a screenshot • Calls your listener to handle the report
  155. Telescope • Press and hold 2 fingers to trigger a

    bug report • Takes a screenshot • Calls your listener to handle the report • Send an email
  156. Telescope • Press and hold 2 fingers to trigger a

    bug report • Takes a screenshot • Calls your listener to handle the report • Send an email • Open a ticket via an API
  157. Telescope • Press and hold 2 fingers to trigger a

    bug report • Takes a screenshot • Calls your listener to handle the report • Send an email • Open a ticket via an API • https://github.com/mattprecious/telescope
  158. LumberYard (src/internal) @Singleton public final class LumberYard { ! @Inject

    public LumberYard() { } }a
  159. LumberYard (src/internal) @Singleton public final class LumberYard { private final

    Deque<Entry> entries = new ArrayDeque<>(BUFFER_SIZE + 1); ! @Inject public LumberYard() { } ! private synchronized void addEntry(Entry entry) { // ... } }a
  160. LumberYard (src/internal) @Singleton public final class LumberYard { private final

    Deque<Entry> entries = new ArrayDeque<>(BUFFER_SIZE + 1); ! @Inject public LumberYard() { } ! public Timber.Tree tree() {
 return (DebugTree) logMessage(priority, tag, message) -> {
 addEntry(new Entry(priority, tag, message));
 };
 }b ! private synchronized void addEntry(Entry entry) { // ... } }a
  161. LumberYard (src/internal) @Singleton public final class LumberYard { private final

    Deque<Entry> entries = new ArrayDeque<>(BUFFER_SIZE + 1); ! @Inject public LumberYard() { } ! public Timber.Tree tree() { // ... }b ! private synchronized void addEntry(Entry entry) { // ... } ! public Observable<File> save() { // ... } }a
  162. BugReportLens (src/internal) public final class BugReportLens implements Lens { private

    final Context context;
 private final LumberYard lumberYard;
 
 private File screenshot;
 
 public BugReportLens(Context context, LumberYard lumberYard) { // ... } }aa
  163. BugReportLens (src/internal) public final class BugReportLens implements Lens { private

    final Context context;
 private final LumberYard lumberYard;
 
 private File screenshot;
 
 public BugReportLens(Context context, LumberYard lumberYard) { // ... } ! @Override public void onCapture(File screenshot) {
 this.screenshot = screenshot;
 // TODO: Show bug report dialog.
 }a }aa
  164. BugReportLens (src/internal) public final class BugReportLens implements Lens { private

    final Context context;
 private final LumberYard lumberYard;
 
 private File screenshot;
 
 public BugReportLens(Context context, LumberYard lumberYard) { // ... } ! @Override public void onCapture(File screenshot) {
 this.screenshot = screenshot;
 // TODO: Show bug report dialog.
 }a ! private void submitReport(Report report, File logs) { // TODO: Create email intent and set recipient. // TODO: Pre-fill body with device and report information. // TODO: Add screenshot and log file as attachment. } }aa
  165. debug_activity_frame (src/internalDebug) <DrawerLayout android:id="@+id/debug_drawer_layout"> 
 <FrameLayout android:id="@+id/debug_content" /> 
 <ScrollView


    android:id="@+id/debug_drawer"
 android:layout_gravity="right" android:background="#ee212121" /> 
 </DrawerLayout>
  166. debug_activity_frame (src/internalDebug) <DrawerLayout android:id="@+id/debug_drawer_layout"> 
 <FrameLayout android:id="@+id/debug_content" /> 
 <ScrollView


    android:id="@+id/debug_drawer"
 android:layout_gravity="right" android:background="#ee212121" /> 
 </DrawerLayout>
  167. debug_activity_frame (src/internalDebug) <DrawerLayout android:id="@+id/debug_drawer_layout"> 
 <com.mattprecious.telescope.TelescopeLayout android:id="@+id/debug_content" /> 
 <ScrollView


    android:id="@+id/debug_drawer"
 android:layout_gravity="right" android:background="#ee212121" /> 
 </DrawerLayout>
  168. DebugAppContainer (src/internalDebug) @Singleton
 public final class DebugAppContainer implements AppContainer {

    ! @Inject public DebugAppContainer() { } ! @Override public ViewGroup bind(final Activity activity) { activity.setContentView(R.layout.debug_activity_frame); ! ViewGroup drawer = findById(activity, R.id.debug_drawer);
 DebugView debugView = new DebugView(activity);
 drawer.addView(debugView); ! return findById(activity, R.id.debug_content); }a }a
  169. DebugAppContainer (src/internalDebug) @Singleton
 public final class DebugAppContainer implements AppContainer {

    ! @Inject public DebugAppContainer() { } ! @Override public ViewGroup bind(final Activity activity) { // ... ! return findById(activity, R.id.debug_content); }a }a
  170. DebugAppContainer (src/internalDebug) @Singleton
 public final class DebugAppContainer implements AppContainer {

    ! @Inject public DebugAppContainer() { } ! @Override public ViewGroup bind(final Activity activity) { // ... ! TelescopeLayout telescopeLayout = findById(activity, R.id.debug_content); return telescopeLayout; }a }a
  171. DebugAppContainer (src/internalDebug) @Singleton
 public final class DebugAppContainer implements AppContainer {

    private final LumberYard lumberYard; ! @Inject public DebugAppContainer(LumberYard lumberYard) { this.lumberYard = lumberYard; }a ! @Override public ViewGroup bind(final Activity activity) { // ... ! TelescopeLayout telescopeLayout = findById(activity, R.id.debug_content); telescopeLayout.setLens(new BugReportLens(activity, lumberYard)); return telescopeLayout; }a }a
  172. internal_activity_frame (src/internalRelease) <com.mattprecious.telescope.TelescopeLayout android:id="@+id/telescope_container" />

  173. TelescopeAppContainer (src/internalRelease) @Singleton
 public final class TelescopeAppContainer implements AppContainer {

    @InjectView(R.id.telescope_container) TelescopeLayout telescopeLayout; ! private final LumberYard lumberYard; ! @Inject public TelescopeAppContainer(LumberYard lumberYard) {
 this.lumberYard = lumberYard;
 }a ! @Override public ViewGroup bind(final Activity activity) {
 activity.setContentView(R.layout.internal_activity_frame);
 ButterKnife.inject(this, activity);
 
 telescopeLayout.setLens(new BugReportLens(activity, lumberYard)); return telescopeLayout; }a }a
  174. TelescopeAppContainer (src/internalRelease) @Singleton
 public final class TelescopeAppContainer implements AppContainer {

    @InjectView(R.id.telescope_container) TelescopeLayout telescopeLayout; ! private final LumberYard lumberYard; ! @Inject public TelescopeAppContainer(LumberYard lumberYard) {
 this.lumberYard = lumberYard;
 }a ! @Override public ViewGroup bind(final Activity activity) {
 activity.setContentView(R.layout.internal_activity_frame);
 ButterKnife.inject(this, activity);
 
 telescopeLayout.setLens(new BugReportLens(activity, lumberYard)); return telescopeLayout; }a }a
  175. TelescopeAppContainer (src/internalRelease) @Singleton
 public final class TelescopeAppContainer implements AppContainer {

    @InjectView(R.id.telescope_container) TelescopeLayout telescopeLayout; ! private final LumberYard lumberYard; ! @Inject public TelescopeAppContainer(LumberYard lumberYard) {
 this.lumberYard = lumberYard;
 }a ! @Override public ViewGroup bind(final Activity activity) {
 activity.setContentView(R.layout.internal_activity_frame);
 ButterKnife.inject(this, activity);
 
 telescopeLayout.setLens(new BugReportLens(activity, lumberYard)); return telescopeLayout; }a }a
  176. InternalReleaseUiModule (src/internalRelease) @Provides @Singleton AppContainer provideAppContainer(
 TelescopeAppContainer telescopeAppContainer) {
 return

    telescopeAppContainer;
 }
  177. Some more tools

  178. Some more tools • Mock push notifications

  179. Some more tools • Mock push notifications • Reset flags

    (finished on-boarding, seen help windows, etc.)
  180. Some more tools • Mock push notifications • Reset flags

    (finished on-boarding, seen help windows, etc.) • Shortcuts
  181. Some more tools • Mock push notifications • Reset flags

    (finished on-boarding, seen help windows, etc.) • Shortcuts • DB operations
  182. Some more tools • Mock push notifications • Reset flags

    (finished on-boarding, seen help windows, etc.) • Shortcuts • DB operations • Show logs
  183. Some more tools • Mock push notifications • Reset flags

    (finished on-boarding, seen help windows, etc.) • Shortcuts • DB operations • Show logs • Debug activity
  184. Some more tools • Mock push notifications • Reset flags

    (finished on-boarding, seen help windows, etc.) • Shortcuts • DB operations • Show logs • Debug activity • Contextual actions
  185. None
  186. None
  187. Final thoughts

  188. Final thoughts • Fork U+2020!

  189. Final thoughts • Fork U+2020! • Your time is valuable

  190. Final thoughts • Fork U+2020! • Your time is valuable

    • Add debug controls!
  191. Debug Builds A NEW HOPE Matt Precious

  192. Debug Builds A NEW HOPE Questions? Matt Precious @mattprec +MatthewPrecious