Share
Embed
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
Transcript
192 slides
-
What we’ll need • Gradle build variants • Dagger •
Retrofit • OkHttp -
What we’ll need • Gradle build variants • Dagger •
Retrofit • OkHttp • Timber -
Build configuration • Types • debug • release • Flavors
• internal • production -
Build configuration • Types • debug • release • Flavors
• internal • production • Variants -
Build configuration • Types • debug • release • Flavors
• internal • production • Variants • internalDebug -
Build configuration • Types • debug • release • Flavors
• internal • production • Variants • internalDebug • internalRelease -
Build configuration • Types • debug • release • Flavors
• internal • production • Variants • internalDebug • internalRelease • productionDebug -
Build configuration • Types • debug • release • Flavors
• internal • production • Variants • internalDebug • internalRelease • productionDebug • productionRelease -
MainActivity (src/main) public final class MainActivity extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); } }a -
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 -
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 -
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 -
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 -
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 -
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 -
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> -
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> -
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> -
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 -
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 -
DebugAppContainer (src/internalDebug) @Singleton public final class DebugAppContainer implements AppContainer {
! @Inject public DebugAppContainer() { } ! @Override public ViewGroup bind(final Activity activity) { }a }a -
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 -
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 -
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 -
ApiModule (src/main) public static final String PRODUCTION_API_URL = "https://api.github.com";
@Provides @Singleton Endpoint provideEndpoint() { return Endpoints.newFixedEndpoint(PRODUCTION_API_URL); } -
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 -
DebugDataModule (src/internalDebug) @Provides @Singleton @ApiEndpoint StringPreference provideEndpointPreference(SharedPreferences preferences) { return
new StringPreference(preferences, “debug_endpoint", ApiEndpoints.STAGING.url); } -
DebugApiModule (src/internalDebug) @Provides @Singleton Endpoint provideEndpoint(@ApiEndpoint StringPreference apiEndpoint) { return
Endpoints.newFixedEndpoint(apiEndpoint.get()); } -
DebugView (src/internalDebug) @Inject @ApiEndpoint StringPreference networkEndpoint; ! private void setupNetworkSection()
{ final ApiEndpoints currentEndpoint = ApiEndpoints.from(networkEndpoint.get()); }aa -
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 -
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 -
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 -
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 -
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 -
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 -
DebugView (src/internalDebug) @Inject @ApiEndpoint StringPreference networkEndpoint; ! private void setupNetworkSection()
{ // ... }aa ! private void setEndpointAndRelaunch(String endpoint) { networkEndpoint.set(endpoint); ProcessPhoenix.triggerRebirth(getContext()); }a -
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 -
Charles • HTTP proxy • Monitor network traffic • Request
• Response • Headers -
Charles • HTTP proxy • Monitor network traffic • Request
• Response • Headers • Intercept and modify requests and responses -
Charles • HTTP proxy • Monitor network traffic • Request
• Response • Headers • Intercept and modify requests and responses • http://www.charlesproxy.com/ -
DataModule (src/main) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app) { return createOkHttpClient(app);
} ! static OkHttpClient createOkHttpClient(Application app) { OkHttpClient client = new OkHttpClient(); // ... return client; } -
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)); } } -
DebugDataModule (src/internalDebug) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app) { OkHttpClient client
= DataModule.createOkHttpClient(app); return client; }a -
DebugDataModule (src/internalDebug) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app, NetworkProxyPreference networkProxy) {
OkHttpClient client = DataModule.createOkHttpClient(app); client.setProxy(networkProxy.getProxy()); return client; }a -
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 -
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 -
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 -
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 -
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 -
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 -
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); } -
GithubService (src/main) public interface GithubService { @GET("/search/repositories") // Observable<RepositoriesResponse> repositories(
@Query("q") SearchQuery query, @Query("sort") Sort sort, @Query("order") Order order); } -
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 -
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 -
ApiEndpoints (src/internalDebug) public enum ApiEndpoints { // ... public static
boolean isMockMode(String endpoint) { return from(endpoint) == MOCK_MODE; }a }aa -
DebugDataModule (src/internalDebug) @Provides @Singleton @IsMockMode boolean provideIsMockMode(@ApiEndpoint StringPreference endpoint) {
return ApiEndpoints.isMockMode(endpoint.get()); } -
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(); ! // ... -
MockGithubService (src/internalDebug) @Singleton public final class MockGithubService implements GithubService {
@Inject MockGithubService() { } }a -
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 -
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); } -
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" /> -
DebugApiModule @Provides @Singleton MockRestAdapter provideMockRestAdapter(RestAdapter restAdapter, SharedPreferences preferences) { MockRestAdapter
mockRestAdapter = MockRestAdapter.from(restAdapter); AndroidMockValuePersistence.install(mockRestAdapter, preferences); return mockRestAdapter; } -
DebugView (src/internalDebug) @InjectView(R.id.debug_network_delay) Spinner networkDelayView; ! private void setupNetworkSection() {
// ... ! final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext()); networkDelayView.setAdapter(delayAdapter); }aa -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
DebugView (src/internalDebug) private <T extends Enum<T>> void configureResponseSpinner(Spinner spinner, final
Class<T> responseClass) { }aa -
DebugView (src/internalDebug) private <T extends Enum<T>> void configureResponseSpinner(Spinner spinner, final
Class<T> responseClass) { }aa -
DebugView (src/internalDebug) private <T extends Enum<T>> void configureResponseSpinner(Spinner spinner, final
Class<T> responseClass) { }aa -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
DebugDataModule (src/internalDebug) @Provides @Singleton @CaptureIntents BooleanPreference provideCaptureIntentsPreference(SharedPreferences preferences) { return
new BooleanPreference(preferences, "debug_capture_intents", DEFAULT_CAPTURE_INTENTS); } -
DebugIntentFactory (src/internalDebug) @Singleton public final class DebugIntentFactory implements IntentFactory {
@Inject public DebugIntentFactory() { } }aa -
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 -
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 -
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 -
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 -
Telescope • Press and hold 2 fingers to trigger a
bug report • Takes a screenshot -
Telescope • Press and hold 2 fingers to trigger a
bug report • Takes a screenshot • Calls your listener to handle the report -
Telescope • Press and hold 2 fingers to trigger a
bug report • Takes a screenshot • Calls your listener to handle the report • Send an email -
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 -
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 -
LumberYard (src/internal) @Singleton public final class LumberYard { ! @Inject
public LumberYard() { } }a -
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 -
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 -
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 -
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 -
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 -
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 -
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> -
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> -
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> -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
Some more tools • Mock push notifications • Reset flags
(finished on-boarding, seen help windows, etc.) -
Some more tools • Mock push notifications • Reset flags
(finished on-boarding, seen help windows, etc.) • Shortcuts -
Some more tools • Mock push notifications • Reset flags
(finished on-boarding, seen help windows, etc.) • Shortcuts • DB operations -
Some more tools • Mock push notifications • Reset flags
(finished on-boarding, seen help windows, etc.) • Shortcuts • DB operations • Show logs -
Some more tools • Mock push notifications • Reset flags
(finished on-boarding, seen help windows, etc.) • Shortcuts • DB operations • Show logs • Debug activity -
Some more tools • Mock push notifications • Reset flags
(finished on-boarding, seen help windows, etc.) • Shortcuts • DB operations • Show logs • Debug activity • Contextual actions -
Final thoughts • Fork U+2020! • Your time is valuable
• Add debug controls!