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

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.

Matthew Precious

April 10, 2015
Tweet

More Decks by Matthew Precious

Other Decks in Programming

Transcript

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

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

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

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

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

    • internal • production • Variants • internalDebug • internalRelease • productionDebug • productionRelease
  6. MainActivity (src/main) public final class MainActivity extends Activity {
 


    @Override protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState); setContentView(R.layout.main_activity);
 }
 }a
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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>
  14. 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>
  15. 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>
  16. 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
  17. 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
  18. DebugAppContainer (src/internalDebug) @Singleton
 public final class DebugAppContainer implements AppContainer {

    ! @Inject public DebugAppContainer() { } ! @Override public ViewGroup bind(final Activity activity) { }a }a
  19. 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
  20. 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
  21. 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
  22. ApiModule (src/main) public static final String PRODUCTION_API_URL = "https://api.github.com";
 


    @Provides @Singleton Endpoint provideEndpoint() {
 return Endpoints.newFixedEndpoint(PRODUCTION_API_URL);
 }
  23. 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
  24. DebugView (src/internalDebug) @Inject @ApiEndpoint StringPreference networkEndpoint; ! private void setupNetworkSection()

    {
 final ApiEndpoints currentEndpoint = ApiEndpoints.from(networkEndpoint.get());
 }aa
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. DebugView (src/internalDebug) @Inject @ApiEndpoint StringPreference networkEndpoint; ! private void setupNetworkSection()

    {
 // ...
 }aa ! private void setEndpointAndRelaunch(String endpoint) {
 networkEndpoint.set(endpoint);
 
 ProcessPhoenix.triggerRebirth(getContext());
 }a
  32. 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
  33. Charles • HTTP proxy • Monitor network traffic • Request

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

    • Response • Headers • Intercept and modify requests and responses • http://www.charlesproxy.com/
  35. DataModule (src/main) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app) {
 return createOkHttpClient(app);


    } ! static OkHttpClient createOkHttpClient(Application app) {
 OkHttpClient client = new OkHttpClient();
 // ... 
 return client;
 }
  36. 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));
 }
 }
  37. DebugDataModule (src/internalDebug) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app,
 NetworkProxyPreference networkProxy) {


    OkHttpClient client = DataModule.createOkHttpClient(app); 
 client.setProxy(networkProxy.getProxy());
 return client;
 }a
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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);
 }
  45. GithubService (src/main) public interface GithubService {
 @GET("/search/repositories") //
 Observable<RepositoriesResponse> repositories(


    @Query("q") SearchQuery query,
 @Query("sort") Sort sort,
 @Query("order") Order order);
 }
  46. 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
  47. 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
  48. ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 // ...
 public static

    boolean isMockMode(String endpoint) {
 return from(endpoint) == MOCK_MODE;
 }a }aa
  49. 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(); ! // ...
  50. 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
  51. 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);
 }
  52. :)

  53. 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" />
  54. DebugApiModule @Provides @Singleton MockRestAdapter provideMockRestAdapter(RestAdapter restAdapter,
 SharedPreferences preferences) {
 MockRestAdapter

    mockRestAdapter = MockRestAdapter.from(restAdapter);
 AndroidMockValuePersistence.install(mockRestAdapter, preferences);
 return mockRestAdapter;
 }
  55. DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; ! private void setupNetworkSection() {

    // ... ! final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext());
 networkDelayView.setAdapter(delayAdapter);
 }aa
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. 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
  85. Telescope • Press and hold 2 fingers to trigger a

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

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

    bug report • Takes a screenshot • Calls your listener to handle the report • Send an email
  88. 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
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. 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
  95. 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
  96. 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>
  97. 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>
  98. 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
  99. 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
  100. 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
  101. 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
  102. 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
  103. 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
  104. 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
  105. Some more tools • Mock push notifications • Reset flags

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

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

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

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

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

    (finished on-boarding, seen help windows, etc.) • Shortcuts • DB operations • Show logs • Debug activity • Contextual actions