Slide 1

Slide 1 text

Debug Builds A NEW HOPE Matt Precious

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

What we’ll need

Slide 4

Slide 4 text

What we’ll need • Gradle build variants

Slide 5

Slide 5 text

What we’ll need • Gradle build variants • Dagger

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Build configuration

Slide 10

Slide 10 text

Build configuration • Types • debug • release

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Debug drawer

Slide 18

Slide 18 text

Debug drawer

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Activity Content Container Content

Slide 21

Slide 21 text

Activity Content Container Content Debug Drawer Debug View

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

UiModule (src/main) @Provides @Singleton AppContainer provideAppContainer() {
 return AppContainer.DEFAULT;
 }a

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

debug_activity_frame (src/internalDebug) 
 
 


Slide 31

Slide 31 text

debug_activity_frame (src/internalDebug) 
 
 


Slide 32

Slide 32 text

debug_activity_frame (src/internalDebug) 
 
 


Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

DebugUiModule (src/internalDebug) @Provides @Singleton AppContainer provideAppContainer(DebugAppContainer debugAppContainer) {
 return debugAppContainer;
 }a

Slide 40

Slide 40 text

Endpoints

Slide 41

Slide 41 text

Endpoints

Slide 42

Slide 42 text

ApiModule (src/main) public static final String PRODUCTION_API_URL = "https://api.github.com";
 
 @Provides @Singleton Endpoint provideEndpoint() {
 return Endpoints.newFixedEndpoint(PRODUCTION_API_URL);
 }

Slide 43

Slide 43 text

ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 PRODUCTION,
 STAGING,
 CUSTOM,
 }aa

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

DebugDataModule (src/internalDebug) @Provides @Singleton @ApiEndpoint
 StringPreference provideEndpointPreference(SharedPreferences preferences) {
 return new StringPreference(preferences, “debug_endpoint", ApiEndpoints.STAGING.url);
 }

Slide 46

Slide 46 text

DebugApiModule (src/internalDebug) @Provides @Singleton
 Endpoint provideEndpoint(@ApiEndpoint StringPreference apiEndpoint) {
 return Endpoints.newFixedEndpoint(apiEndpoint.get());
 }

Slide 47

Slide 47 text

debug_view_content (src/internalDebug) !

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

DebugView (src/internalDebug) @Inject @ApiEndpoint StringPreference networkEndpoint; ! private void setupNetworkSection() {
 final ApiEndpoints currentEndpoint = ApiEndpoints.from(networkEndpoint.get());
 }aa

Slide 50

Slide 50 text

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 endpointAdapter =
 new EnumAdapter<>(getContext(), ApiEndpoints.class);
 endpointView.setAdapter(endpointAdapter);
 }aa

Slide 51

Slide 51 text

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 endpointAdapter =
 new EnumAdapter<>(getContext(), ApiEndpoints.class);
 endpointView.setAdapter(endpointAdapter);
 }aa

Slide 52

Slide 52 text

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 endpointAdapter =
 new EnumAdapter<>(getContext(), ApiEndpoints.class);
 endpointView.setAdapter(endpointAdapter);
 endpointView.setSelection(currentEndpoint.ordinal());
 }aa

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Charles

Slide 59

Slide 59 text

Charles • HTTP proxy

Slide 60

Slide 60 text

Charles • HTTP proxy • Monitor network traffic

Slide 61

Slide 61 text

Charles • HTTP proxy • Monitor network traffic • Request

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

Charles • HTTP proxy • Monitor network traffic • Request • Response • Headers • Intercept and modify requests and responses • http://www.charlesproxy.com/

Slide 66

Slide 66 text

Network Proxy

Slide 67

Slide 67 text

Network Proxy

Slide 68

Slide 68 text

Network Proxy

Slide 69

Slide 69 text

DataModule (src/main) @Provides @Singleton OkHttpClient provideOkHttpClient(Application app) {
 return createOkHttpClient(app);
 } ! static OkHttpClient createOkHttpClient(Application app) {
 OkHttpClient client = new OkHttpClient();
 // ... 
 return client;
 }

Slide 70

Slide 70 text

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));
 }
 }

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

debug_view_content (src/internalDebug) 


Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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);
 }

Slide 82

Slide 82 text

Mock Mode

Slide 83

Slide 83 text

GithubService (src/main) public interface GithubService {
 @GET("/search/repositories") //
 Observable repositories(
 @Query("q") SearchQuery query,
 @Query("sort") Sort sort,
 @Query("order") Order order);
 }

Slide 84

Slide 84 text

ApiModule (src/main) @Provides @Singleton GithubService provideGithubService(RestAdapter restAdapter) {
 return restAdapter.create(GithubService.class);
 }

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

ApiEndpoints (src/internalDebug) public enum ApiEndpoints {
 // ...
 public static boolean isMockMode(String endpoint) {
 return from(endpoint) == MOCK_MODE;
 }a }aa

Slide 88

Slide 88 text

DebugDataModule (src/internalDebug) @Provides @Singleton @IsMockMode boolean provideIsMockMode(@ApiEndpoint StringPreference endpoint) {
 return ApiEndpoints.isMockMode(endpoint.get());
 }

Slide 89

Slide 89 text

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(); ! // ...

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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);
 }

Slide 93

Slide 93 text

No content

Slide 94

Slide 94 text

:)

Slide 95

Slide 95 text

:) Loading Error Empty One result Lots of results

Slide 96

Slide 96 text

:) Loading Error Empty One result Lots of results ✔

Slide 97

Slide 97 text

:) Loading Error Empty One result Lots of results ✔

Slide 98

Slide 98 text

debug_view_content (src/internalDebug) 
 
 
 


Slide 99

Slide 99 text

DebugApiModule @Provides @Singleton MockRestAdapter provideMockRestAdapter(RestAdapter restAdapter,
 SharedPreferences preferences) {
 MockRestAdapter mockRestAdapter = MockRestAdapter.from(restAdapter);
 AndroidMockValuePersistence.install(mockRestAdapter, preferences);
 return mockRestAdapter;
 }

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

:) Loading Error Empty One result Lots of results ✔

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

new RepositoriesResponse(Arrays.asList(
 BUTTERKNIFE,
 DAGGER,
 OKHTTP,
 OKIO,
 PICASSO,
 RETROFIT,
 TELESCOPE,
 U2020))

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

MockGithubService (src/internalDebug) @Singleton
 public final class MockGithubService implements GithubService {
 // ...
 
 public > T getResponse(Class responseClass) { // } 
 public > void setResponse(Class responseClass, T value) { // }
 
 @Override public Observable 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

Slide 121

Slide 121 text

debug_view_content (src/internalDebug) 


Slide 122

Slide 122 text

DebugView (src/internalDebug) private > void configureResponseSpinner(Spinner spinner,
 final Class responseClass) {
 }aa

Slide 123

Slide 123 text

DebugView (src/internalDebug) private > void configureResponseSpinner(Spinner spinner,
 final Class responseClass) {
 }aa

Slide 124

Slide 124 text

DebugView (src/internalDebug) private > void configureResponseSpinner(Spinner spinner,
 final Class responseClass) {
 }aa

Slide 125

Slide 125 text

DebugView (src/internalDebug) private > void configureResponseSpinner(Spinner spinner,
 final Class responseClass) {
 final EnumAdapter adapter = new EnumAdapter<>(getContext(), responseClass); spinner.setAdapter(adapter); }aa

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

DebugView (src/internalDebug) @Inject @IsMockMode boolean isMockMode; @Inject MockGithubService mockGithubService; ! private > void configureResponseSpinner(Spinner spinner,
 final Class responseClass) {
 final EnumAdapter 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

Slide 129

Slide 129 text

DebugView (src/internalDebug) @InjectView(R.id.debug_repositories_response) Spinner repositoriesResponseView; ! private void setupMockBehaviorSection() {
 configureResponseSpinner(repositoriesResponseView, MockRepositoriesResponse.class);
 } ! private > void configureResponseSpinner(Spinner spinner,
 final Class responseClass) {
 // ...
 }aa

Slide 130

Slide 130 text

Intent Capturing

Slide 131

Slide 131 text

Intent Capturing

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

DataModule (src/main) @Provides @Singleton IntentFactory provideIntentFactory() {
 return IntentFactory.REAL;
 }

Slide 136

Slide 136 text

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

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

DebugDataModule (src/internalDebug) @Provides @Singleton @CaptureIntents
 BooleanPreference provideCaptureIntentsPreference(SharedPreferences preferences) {
 return new BooleanPreference(preferences, "debug_capture_intents", DEFAULT_CAPTURE_INTENTS);
 }

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

DebugDataModule (src/internalDebug) @Provides @Singleton IntentFactory provideIntentFactory(DebugIntentFactory debugIntentFactory) {
 return debugIntentFactory;
 }

Slide 148

Slide 148 text

Bug Reporting

Slide 149

Slide 149 text

Bug Reporting

Slide 150

Slide 150 text

Bug Reporting

Slide 151

Slide 151 text

Telescope

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

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

Slide 158

Slide 158 text

LumberYard (src/internal) @Singleton public final class LumberYard { ! @Inject public LumberYard() { } }a

Slide 159

Slide 159 text

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

Slide 160

Slide 160 text

LumberYard (src/internal) @Singleton public final class LumberYard { private final Deque 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

Slide 161

Slide 161 text

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

Slide 162

Slide 162 text

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

Slide 163

Slide 163 text

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

Slide 164

Slide 164 text

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

Slide 165

Slide 165 text

debug_activity_frame (src/internalDebug) 
 
 


Slide 166

Slide 166 text

debug_activity_frame (src/internalDebug) 
 
 


Slide 167

Slide 167 text

debug_activity_frame (src/internalDebug) 
 
 


Slide 168

Slide 168 text

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

Slide 169

Slide 169 text

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

Slide 170

Slide 170 text

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

Slide 171

Slide 171 text

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

Slide 172

Slide 172 text

internal_activity_frame (src/internalRelease)

Slide 173

Slide 173 text

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

Slide 174

Slide 174 text

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

Slide 175

Slide 175 text

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

Slide 176

Slide 176 text

InternalReleaseUiModule (src/internalRelease) @Provides @Singleton AppContainer provideAppContainer(
 TelescopeAppContainer telescopeAppContainer) {
 return telescopeAppContainer;
 }

Slide 177

Slide 177 text

Some more tools

Slide 178

Slide 178 text

Some more tools • Mock push notifications

Slide 179

Slide 179 text

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

Slide 180

Slide 180 text

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

Slide 181

Slide 181 text

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

Slide 182

Slide 182 text

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

Slide 183

Slide 183 text

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

Slide 184

Slide 184 text

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

Slide 185

Slide 185 text

No content

Slide 186

Slide 186 text

No content

Slide 187

Slide 187 text

Final thoughts

Slide 188

Slide 188 text

Final thoughts • Fork U+2020!

Slide 189

Slide 189 text

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

Slide 190

Slide 190 text

Final thoughts • Fork U+2020! • Your time is valuable • Add debug controls!

Slide 191

Slide 191 text

Debug Builds A NEW HOPE Matt Precious

Slide 192

Slide 192 text

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