Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Debug Builds: A New Hope (Droidcon NY 2015)

Debug Builds: A New Hope (Droidcon NY 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

August 28, 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 • Gradle build variants • Dagger (v1)

    • Retrofit (v2) • OkHttp • Timber
  4. Build configuration

  5. Build configuration • Types • debug • release

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

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

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

  9. Debug drawer

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


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

  12. Activity Content Container Content Debug Drawer Debug View

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

  14. 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
  15. UiModule (src/main) @Provides @Singleton AppContainer provideAppContainer() {
 return AppContainer.DEFAULT;
 }a

  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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>
  22. 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>
  23. 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>
  24. 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
  25. 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
  26. DebugAppContainer (src/internalDebug) @Singleton
 public final class DebugAppContainer implements AppContainer {

    @Inject public DebugAppContainer() { } @Override public ViewGroup bind(final Activity activity) { }a }a
  27. 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
  28. 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
  29. 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
  30. DebugUiModule (src/internalDebug) @Provides @Singleton AppContainer provideAppContainer(DebugAppContainer debugAppContainer) {
 return debugAppContainer;


    }a
  31. Endpoints

  32. Endpoints

  33. ApiModule (src/main) public static final HttpUrl PRODUCTION_API_URL = HttpUrl.parse("https://api.github.com");
 


    @Provides @Singleton HttpUrl provideBaseUrl() {
 return PRODUCTION_API_URL;
 }
  34. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 PRODUCTION,
 STAGING,
 CUSTOM,
 }aa

  35. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 PRODUCTION("Production", ApiModule.PRODUCTION_API_URL.toString()),
 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
  36. DebugDataModule (src/internalDebug) @Provides @Singleton @ApiEndpoint
 StringPreference provideEndpointPreference(SharedPreferences preferences) {
 return

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

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

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

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


    final ApiEndpoints currentEndpoint = ApiEndpoints.from(networkEndpoint.get());
 }aa
  41. DebugView (src/internalDebug) @Bind(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
  42. DebugView (src/internalDebug) @Bind(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
  43. DebugView (src/internalDebug) @Bind(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
  44. DebugView (src/internalDebug) @Bind(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
  45. DebugView (src/internalDebug) @Bind(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
  46. DebugView (src/internalDebug) @Bind(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
  47. DebugView (src/internalDebug) @Inject @ApiEndpoint StringPreference networkEndpoint; private void setupNetworkSection() {


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


    // ...
 }aa private void setEndpointAndRelaunch(String endpoint) {
 networkEndpoint.set(endpoint);
 
 ProcessPhoenix.triggerRebirth(getContext());
 }a https://github.com/JakeWharton/ProcessPhoenix
  49. Network Proxy

  50. Network Proxy

  51. Network Proxy

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


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

    NetworkProxyPreference(SharedPreferences preferences, String key) {
 super(preferences, key);
 }
 
 public @Nullable Proxy getProxy() {
 // TODO: Convert String value to Proxy and return.
 }
 }
  54. DebugDataModule (src/internalDebug) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app) {
 OkHttpClient client

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


    OkHttpClient client = DataModule.createOkHttpClient(app); 
 client.setProxy(networkProxy.getProxy());
 return client;
 }a
  56. 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
  57. debug_view_content (src/internalDebug) <TextView style="@style/Widget.U2020.DebugDrawer.RowTitle"
 android:text="Proxy"
 /> 
 <Spinner android:id="@+id/debug_network_proxy" />

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

  59. DebugView (src/internalDebug) @Bind(R.id.debug_network_proxy) Spinner networkProxyView; @Inject NetworkProxyPreference networkProxy; private void

    setupNetworkSection() { // ... 
 final ProxyAdapter proxyAdapter = new ProxyAdapter(getContext(), networkProxy);
 networkProxyView.setAdapter(proxyAdapter);
 }aa
  60. DebugView (src/internalDebug) @Bind(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
  61. DebugView (src/internalDebug) @Bind(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
  62. DebugView (src/internalDebug) @Bind(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
  63. DebugView (src/internalDebug) @Bind(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
  64. DebugView (src/internalDebug) @Bind(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);
 }
  65. Mock Mode

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


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


    }
  68. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 PRODUCTION("Production", ApiModule.PRODUCTION_API_URL.toString()),
 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
  69. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 PRODUCTION("Production", ApiModule.PRODUCTION_API_URL.toString()),
 STAGING("Staging", "https://api.staging.github.com/"),

    MOCK_MODE("Mock mode", "http://localhost/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
  70. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 // ...
 public static

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


    return ApiEndpoints.isMockMode(endpoint.get());
 }
  72. 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(); // ...
  73. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


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


    
 @Inject MockGithubService() { }
 
 @Override public Observable<Result<RepositoriesResponse>> repositories( SearchQuery query, Sort sort, Order order) { return Observable.just(Result.response(Response.success( new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,
 DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020)))));
 }
 }a
  75. DebugApiModule (src/internalDebug) @Provides @Singleton
 GithubService provideGithubService(Retrofit retrofit, MockRetrofit mockRetrofit, @IsMockMode

    boolean isMockMode, MockGithubService mockService) {
 if (isMockMode) {
 return mockRetrofit.create(GithubService.class, mockService);
 } 
 return retrofit.create(GithubService.class);
 }
  76. None
  77. :)

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

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

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

  81. 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" />
  82. DebugApiModule (src/internalDebug) @Provides @Singleton NetworkBehavior provideNetworkBehavior(/* preferences */) { NetworkBehavior

    behavior = NetworkBehavior.create(); // TODO: Restore behavior values from preferences. return behavior; } @Provides @Singleton MockRetrofit provideMockRestAdapter(NetworkBehavior behavior) {
 return new MockRetrofit(behavior, RxJavaBehaviorAdapter.create());
 }
  83. DebugView (src/internalDebug) private void setupNetworkSection() { // ... }aa

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

    ... final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 }aa
  85. DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; @Inject NetworkBehavior behavior; private void

    setupNetworkSection() { // ... final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 networkDelayView.setSelection(
 NetworkDelayAdapter.getPositionForValue(behavior.delay(MILLISECONDS))); }aa
  86. DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; @Inject NetworkBehavior behavior; private void

    setupNetworkSection() { // ... final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 networkDelayView.setSelection(
 NetworkDelayAdapter.getPositionForValue(behavior.delay(MILLISECONDS))); }aa
  87. DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; @Inject NetworkBehavior behavior; @Inject @NetworkDelay

    LongPreference networkDelay; private void setupNetworkSection() { // ... final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 networkDelayView.setSelection(
 NetworkDelayAdapter.getPositionForValue(behavior.delay(MILLISECONDS)));
 networkDelayView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 int selected = delayAdapter.getItem(position);
 if (selected != behavior.delay(MILLISECONDS)) {
 behavior.setDelay(selected, MILLISECONDS); networkDelay.set(selected);
 }a
 }); }aa
  88. DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; @Inject NetworkBehavior behavior; @Inject @NetworkDelay

    LongPreference networkDelay; private void setupNetworkSection() { // ... final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 networkDelayView.setSelection(
 NetworkDelayAdapter.getPositionForValue(behavior.delay(MILLISECONDS)));
 networkDelayView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 int selected = delayAdapter.getItem(position);
 if (selected != behavior.delay(MILLISECONDS)) {
 behavior.setDelay(selected, MILLISECONDS); networkDelay.set(selected);
 }a
 }); }aa
  89. DebugView (src/internalDebug) @InjectView(R.id.debug_network_error) Spinner networkErrorView; @Inject NetworkBehavior behavior; @Inject @NetworkFailurePercent

    IntPreference networkFailurePercent; private void setupNetworkSection() { // ... final NetworkErrorAdapter errorAdapter = new NetworkErrorAdapter(getContext());
 networkErrorView.setAdapter(errorAdapter);
 networkErrorView.setSelection(
 NetworkErrorAdapter.getPositionForValue(behavior.failurePercent()));
 networkErrorView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 int selected = errorAdapter.getItem(position);
 if (selected != behavior.failurePercent()) {
 behavior.setFailurePercent(selected); networkFailurePercent.set(selected);
 }a
 }); }aa
  90. DebugView (src/internalDebug) @InjectView(R.id.debug_network_error) Spinner networkErrorView; @Inject NetworkBehavior behavior; @Inject @NetworkFailurePercent

    IntPreference networkFailurePercent; private void setupNetworkSection() { // ... final NetworkErrorAdapter errorAdapter = new NetworkErrorAdapter(getContext());
 networkErrorView.setAdapter(errorAdapter);
 networkErrorView.setSelection(
 NetworkErrorAdapter.getPositionForValue(behavior.failurePercent()));
 networkErrorView.setOnItemSelectedListener((adapterView, view, position, id) -> {
 int selected = errorAdapter.getItem(position);
 if (selected != behavior.failurePercent()) {
 behavior.setFailurePercent(selected); networkFailurePercent.set(selected);
 }a
 }); }aa
  91. :) Loading Error Empty One result Lots of results ✔

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

    ✔ ✔
  93. 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(Result.response(Response.success( new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,
 DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020)))));
 }a
 }aa
  94. 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(Result.response(Response.success( new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,
 DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020)))));
 }a
 }aa
  95. new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,
 DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020))

  96. 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
  97. 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
  98. MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {


    // ...
 }aa
  99. 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
  100. 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
  101. 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
  102. 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
  103. 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(Result.response(Response.success(response)));
 }a
 }aa
  104. debug_view_content (src/internalDebug) <TextView style="@style/Widget.U2020.DebugDrawer.RowTitle"
 android:text="Repositories"
 /> 
 <Spinner android:id="@+id/debug_repositories_response" />

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

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

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

    Class<T> responseClass) {
 }aa
  108. 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
  109. 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
  110. 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
  111. 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
  112. DebugView (src/internalDebug) @Bind(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
  113. Intent Capturing

  114. Intent Capturing

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

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

  117. 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
  118. DataModule (src/main) @Provides @Singleton IntentFactory provideIntentFactory() {
 return IntentFactory.REAL;
 }

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

  120. 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
  121. 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
  122. 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
  123. 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
  124. DebugDataModule (src/internalDebug) @Provides @Singleton @CaptureIntents
 BooleanPreference provideCaptureIntentsPreference(SharedPreferences preferences) {
 return

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


    
 @Inject public DebugIntentFactory() { }
 }aa
  126. 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
  127. 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
  128. 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
  129. 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
  130. DebugDataModule (src/internalDebug) @Provides @Singleton IntentFactory provideIntentFactory(DebugIntentFactory debugIntentFactory) {
 return debugIntentFactory;


    }
  131. Bug Reporting

  132. Bug Reporting

  133. Bug Reporting

  134. 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
  135. LumberYard (src/internal) @Singleton public final class LumberYard { @Inject public

    LumberYard() { } }a
  136. 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
  137. 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
  138. 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
  139. 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
  140. 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
  141. 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
  142. 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>
  143. 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>
  144. 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>
  145. 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
  146. 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
  147. 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
  148. 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
  149. internal_activity_frame (src/internalRelease) <com.mattprecious.telescope.TelescopeLayout android:id="@+id/telescope_container" />

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

    @Bind(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.bind(this, activity);
 
 telescopeLayout.setLens(new BugReportLens(activity, lumberYard)); return telescopeLayout; }a }a
  151. TelescopeAppContainer (src/internalRelease) @Singleton
 public final class TelescopeAppContainer implements AppContainer {

    @Bind(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.bind(this, activity);
 
 telescopeLayout.setLens(new BugReportLens(activity, lumberYard)); return telescopeLayout; }a }a
  152. TelescopeAppContainer (src/internalRelease) @Singleton
 public final class TelescopeAppContainer implements AppContainer {

    @Bind(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.bind(this, activity);
 
 telescopeLayout.setLens(new BugReportLens(activity, lumberYard)); return telescopeLayout; }a }a
  153. InternalReleaseUiModule (src/internalRelease) @Provides @Singleton AppContainer provideAppContainer(
 TelescopeAppContainer telescopeAppContainer) {
 return

    telescopeAppContainer;
 }
  154. Some more tools • Mock push notifications • Reset flags

    (finished on-boarding, seen help windows, etc.) • Shortcuts • DB operations • Show logs • Debug activity • Contextual actions
  155. None
  156. None
  157. Final thoughts • Fork U+2020! • Your time is valuable

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

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