Share
Embed
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.
Matthew Precious
August 28, 2015
Transcript
159 slides
-
What we’ll need • Gradle build variants • Dagger (v1)
• Retrofit (v2) • OkHttp • Timber -
Build configuration • Types • debug • release • Flavors
• internal • production -
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 HttpUrl PRODUCTION_API_URL = HttpUrl.parse("https://api.github.com");
@Provides @Singleton HttpUrl provideBaseUrl() { return PRODUCTION_API_URL; } -
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 -
DebugDataModule (src/internalDebug) @Provides @Singleton @ApiEndpoint StringPreference provideEndpointPreference(SharedPreferences preferences) { return
new StringPreference(preferences, “debug_endpoint", ApiEndpoints.STAGING.url); } -
DebugApiModule (src/internalDebug) @Provides @Singleton HttpUrl provideBaseUrl(@ApiEndpoint StringPreference apiEndpoint) { return
HttpUrl.parse(apiEndpoint.get()); } -
DebugView (src/internalDebug) @Inject @ApiEndpoint StringPreference networkEndpoint; private void setupNetworkSection() {
final ApiEndpoints currentEndpoint = ApiEndpoints.from(networkEndpoint.get()); }aa -
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 -
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 -
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 -
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 -
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 -
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 -
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://github.com/JakeWharton/ProcessPhoenix -
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() { // TODO: Convert String value to Proxy and return. } } -
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) @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 -
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 -
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 -
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 -
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 -
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); } -
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); } -
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 -
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 -
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<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 -
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); } -
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 (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()); } -
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 NetworkBehavior behavior; private void
setupNetworkSection() { // ... final NetworkDelayAdapter delayAdapter = new NetworkDelayAdapter(getContext()); networkDelayView.setAdapter(delayAdapter); networkDelayView.setSelection( NetworkDelayAdapter.getPositionForValue(behavior.delay(MILLISECONDS))); }aa -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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(Result.response(Response.success(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) @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 -
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 • 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 { 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 {
@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 -
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 -
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 -
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!