diff --git a/samples/browseable/AgendaData/Application/AndroidManifest.xml b/samples/browseable/AgendaData/Application/AndroidManifest.xml new file mode 100644 index 000000000..b5bebc39c --- /dev/null +++ b/samples/browseable/AgendaData/Application/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/AgendaData/Application/res/drawable-hdpi/ic_launcher.png b/samples/browseable/AgendaData/Application/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..0564717bb Binary files /dev/null and b/samples/browseable/AgendaData/Application/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/browseable/AgendaData/Application/res/drawable-hdpi/tile.9.png b/samples/browseable/AgendaData/Application/res/drawable-hdpi/tile.9.png new file mode 100644 index 000000000..135862883 Binary files /dev/null and b/samples/browseable/AgendaData/Application/res/drawable-hdpi/tile.9.png differ diff --git a/samples/browseable/AgendaData/Application/res/drawable-mdpi/ic_launcher.png b/samples/browseable/AgendaData/Application/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..0f4034743 Binary files /dev/null and b/samples/browseable/AgendaData/Application/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/browseable/AgendaData/Application/res/drawable-nodpi/nobody.png b/samples/browseable/AgendaData/Application/res/drawable-nodpi/nobody.png new file mode 100644 index 000000000..5a33d60e0 Binary files /dev/null and b/samples/browseable/AgendaData/Application/res/drawable-nodpi/nobody.png differ diff --git a/samples/browseable/AgendaData/Application/res/drawable-xhdpi/ic_launcher.png b/samples/browseable/AgendaData/Application/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..d7705cfb0 Binary files /dev/null and b/samples/browseable/AgendaData/Application/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/browseable/AgendaData/Application/res/drawable-xxhdpi/ic_launcher.png b/samples/browseable/AgendaData/Application/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..f07299fd2 Binary files /dev/null and b/samples/browseable/AgendaData/Application/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/samples/browseable/AgendaData/Application/res/layout/activity_main.xml b/samples/browseable/AgendaData/Application/res/layout/activity_main.xml new file mode 100755 index 000000000..be1aa49d9 --- /dev/null +++ b/samples/browseable/AgendaData/Application/res/layout/activity_main.xml @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/samples/browseable/AgendaData/Application/res/layout/main.xml b/samples/browseable/AgendaData/Application/res/layout/main.xml new file mode 100644 index 000000000..8e82cdd45 --- /dev/null +++ b/samples/browseable/AgendaData/Application/res/layout/main.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/Quiz/Application/res/layout/question_status_element.xml b/samples/browseable/Quiz/Application/res/layout/question_status_element.xml new file mode 100644 index 000000000..280f44ad7 --- /dev/null +++ b/samples/browseable/Quiz/Application/res/layout/question_status_element.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/samples/browseable/Quiz/Application/res/values-sw600dp/template-dimens.xml b/samples/browseable/Quiz/Application/res/values-sw600dp/template-dimens.xml new file mode 100644 index 000000000..22074a2bd --- /dev/null +++ b/samples/browseable/Quiz/Application/res/values-sw600dp/template-dimens.xml @@ -0,0 +1,24 @@ + + + + + + + @dimen/margin_huge + @dimen/margin_medium + + diff --git a/samples/browseable/Quiz/Application/res/values-sw600dp/template-styles.xml b/samples/browseable/Quiz/Application/res/values-sw600dp/template-styles.xml new file mode 100644 index 000000000..03d197418 --- /dev/null +++ b/samples/browseable/Quiz/Application/res/values-sw600dp/template-styles.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/samples/browseable/Quiz/Application/res/values-v11/template-styles.xml b/samples/browseable/Quiz/Application/res/values-v11/template-styles.xml new file mode 100644 index 000000000..8c1ea66f2 --- /dev/null +++ b/samples/browseable/Quiz/Application/res/values-v11/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/samples/browseable/Quiz/Application/res/values/template-dimens.xml b/samples/browseable/Quiz/Application/res/values/template-dimens.xml new file mode 100644 index 000000000..39e710b5c --- /dev/null +++ b/samples/browseable/Quiz/Application/res/values/template-dimens.xml @@ -0,0 +1,32 @@ + + + + + + + 4dp + 8dp + 16dp + 32dp + 64dp + + + + @dimen/margin_medium + @dimen/margin_medium + + diff --git a/samples/browseable/Quiz/Application/res/values/template-styles.xml b/samples/browseable/Quiz/Application/res/values/template-styles.xml new file mode 100644 index 000000000..6e7d593dd --- /dev/null +++ b/samples/browseable/Quiz/Application/res/values/template-styles.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/samples/browseable/Quiz/Application/src/com.example.android.common/activities/SampleActivityBase.java b/samples/browseable/Quiz/Application/src/com.example.android.common/activities/SampleActivityBase.java new file mode 100644 index 000000000..3228927b7 --- /dev/null +++ b/samples/browseable/Quiz/Application/src/com.example.android.common/activities/SampleActivityBase.java @@ -0,0 +1,52 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.example.android.common.activities; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogWrapper; + +/** + * Base launcher activity, to handle most of the common plumbing for samples. + */ +public class SampleActivityBase extends FragmentActivity { + + public static final String TAG = "SampleActivityBase"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void onStart() { + super.onStart(); + initializeLogging(); + } + + /** Set up targets to receive log data */ + public void initializeLogging() { + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + // Wraps Android's native log framework + LogWrapper logWrapper = new LogWrapper(); + Log.setLogNode(logWrapper); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/Quiz/Application/src/com.example.android.common/logger/Log.java b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/Log.java new file mode 100644 index 000000000..17503c568 --- /dev/null +++ b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/Log.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Helper class for a list (or tree) of LoggerNodes. + * + *

When this is set as the head of the list, + * an instance of it can function as a drop-in replacement for {@link android.util.Log}. + * Most of the methods in this class server only to map a method call in Log to its equivalent + * in LogNode.

+ */ +public class Log { + // Grabbing the native values from Android's native logging facilities, + // to make for easy migration and interop. + public static final int NONE = -1; + public static final int VERBOSE = android.util.Log.VERBOSE; + public static final int DEBUG = android.util.Log.DEBUG; + public static final int INFO = android.util.Log.INFO; + public static final int WARN = android.util.Log.WARN; + public static final int ERROR = android.util.Log.ERROR; + public static final int ASSERT = android.util.Log.ASSERT; + + // Stores the beginning of the LogNode topology. + private static LogNode mLogNode; + + /** + * Returns the next LogNode in the linked list. + */ + public static LogNode getLogNode() { + return mLogNode; + } + + /** + * Sets the LogNode data will be sent to. + */ + public static void setLogNode(LogNode node) { + mLogNode = node; + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void println(int priority, String tag, String msg, Throwable tr) { + if (mLogNode != null) { + mLogNode.println(priority, tag, msg, tr); + } + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + */ + public static void println(int priority, String tag, String msg) { + println(priority, tag, msg, null); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void v(String tag, String msg, Throwable tr) { + println(VERBOSE, tag, msg, tr); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void v(String tag, String msg) { + v(tag, msg, null); + } + + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void d(String tag, String msg, Throwable tr) { + println(DEBUG, tag, msg, tr); + } + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void d(String tag, String msg) { + d(tag, msg, null); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void i(String tag, String msg, Throwable tr) { + println(INFO, tag, msg, tr); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void i(String tag, String msg) { + i(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, String msg, Throwable tr) { + println(WARN, tag, msg, tr); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void w(String tag, String msg) { + w(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, Throwable tr) { + w(tag, null, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void e(String tag, String msg, Throwable tr) { + println(ERROR, tag, msg, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void e(String tag, String msg) { + e(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, String msg, Throwable tr) { + println(ASSERT, tag, msg, tr); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void wtf(String tag, String msg) { + wtf(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, Throwable tr) { + wtf(tag, null, tr); + } +} diff --git a/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogFragment.java b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogFragment.java new file mode 100644 index 000000000..b302acd4b --- /dev/null +++ b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogFragment.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.logger; + +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +/** + * Simple fraggment which contains a LogView and uses is to output log data it receives + * through the LogNode interface. + */ +public class LogFragment extends Fragment { + + private LogView mLogView; + private ScrollView mScrollView; + + public LogFragment() {} + + public View inflateViews() { + mScrollView = new ScrollView(getActivity()); + ViewGroup.LayoutParams scrollParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mScrollView.setLayoutParams(scrollParams); + + mLogView = new LogView(getActivity()); + ViewGroup.LayoutParams logParams = new ViewGroup.LayoutParams(scrollParams); + logParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + mLogView.setLayoutParams(logParams); + mLogView.setClickable(true); + mLogView.setFocusable(true); + mLogView.setTypeface(Typeface.MONOSPACE); + + // Want to set padding as 16 dips, setPadding takes pixels. Hooray math! + int paddingDips = 16; + double scale = getResources().getDisplayMetrics().density; + int paddingPixels = (int) ((paddingDips * (scale)) + .5); + mLogView.setPadding(paddingPixels, paddingPixels, paddingPixels, paddingPixels); + mLogView.setCompoundDrawablePadding(paddingPixels); + + mLogView.setGravity(Gravity.BOTTOM); + mLogView.setTextAppearance(getActivity(), android.R.style.TextAppearance_Holo_Medium); + + mScrollView.addView(mLogView); + return mScrollView; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View result = inflateViews(); + + mLogView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + return result; + } + + public LogView getLogView() { + return mLogView; + } +} \ No newline at end of file diff --git a/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogNode.java b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogNode.java new file mode 100644 index 000000000..bc37cabc0 --- /dev/null +++ b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogNode.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Basic interface for a logging system that can output to one or more targets. + * Note that in addition to classes that will output these logs in some format, + * one can also implement this interface over a filter and insert that in the chain, + * such that no targets further down see certain data, or see manipulated forms of the data. + * You could, for instance, write a "ToHtmlLoggerNode" that just converted all the log data + * it received to HTML and sent it along to the next node in the chain, without printing it + * anywhere. + */ +public interface LogNode { + + /** + * Instructs first LogNode in the list to print the log data provided. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public void println(int priority, String tag, String msg, Throwable tr); + +} diff --git a/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogView.java b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogView.java new file mode 100644 index 000000000..c01542b91 --- /dev/null +++ b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.app.Activity; +import android.content.Context; +import android.util.*; +import android.widget.TextView; + +/** Simple TextView which is used to output log data received through the LogNode interface. +*/ +public class LogView extends TextView implements LogNode { + + public LogView(Context context) { + super(context); + } + + public LogView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LogView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Formats the log data and prints it out to the LogView. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + + + String priorityStr = null; + + // For the purposes of this View, we want to print the priority as readable text. + switch(priority) { + case android.util.Log.VERBOSE: + priorityStr = "VERBOSE"; + break; + case android.util.Log.DEBUG: + priorityStr = "DEBUG"; + break; + case android.util.Log.INFO: + priorityStr = "INFO"; + break; + case android.util.Log.WARN: + priorityStr = "WARN"; + break; + case android.util.Log.ERROR: + priorityStr = "ERROR"; + break; + case android.util.Log.ASSERT: + priorityStr = "ASSERT"; + break; + default: + break; + } + + // Handily, the Log class has a facility for converting a stack trace into a usable string. + String exceptionStr = null; + if (tr != null) { + exceptionStr = android.util.Log.getStackTraceString(tr); + } + + // Take the priority, tag, message, and exception, and concatenate as necessary + // into one usable line of text. + final StringBuilder outputBuilder = new StringBuilder(); + + String delimiter = "\t"; + appendIfNotNull(outputBuilder, priorityStr, delimiter); + appendIfNotNull(outputBuilder, tag, delimiter); + appendIfNotNull(outputBuilder, msg, delimiter); + appendIfNotNull(outputBuilder, exceptionStr, delimiter); + + // In case this was originally called from an AsyncTask or some other off-UI thread, + // make sure the update occurs within the UI thread. + ((Activity) getContext()).runOnUiThread( (new Thread(new Runnable() { + @Override + public void run() { + // Display the text we just generated within the LogView. + appendToLog(outputBuilder.toString()); + } + }))); + + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } + + public LogNode getNext() { + return mNext; + } + + public void setNext(LogNode node) { + mNext = node; + } + + /** Takes a string and adds to it, with a separator, if the bit to be added isn't null. Since + * the logger takes so many arguments that might be null, this method helps cut out some of the + * agonizing tedium of writing the same 3 lines over and over. + * @param source StringBuilder containing the text to append to. + * @param addStr The String to append + * @param delimiter The String to separate the source and appended strings. A tab or comma, + * for instance. + * @return The fully concatenated String as a StringBuilder + */ + private StringBuilder appendIfNotNull(StringBuilder source, String addStr, String delimiter) { + if (addStr != null) { + if (addStr.length() == 0) { + delimiter = ""; + } + + return source.append(addStr).append(delimiter); + } + return source; + } + + // The next LogNode in the chain. + LogNode mNext; + + /** Outputs the string as a new line of log data in the LogView. */ + public void appendToLog(String s) { + append("\n" + s); + } + + +} diff --git a/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogWrapper.java b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogWrapper.java new file mode 100644 index 000000000..16a9e7ba2 --- /dev/null +++ b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/LogWrapper.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.util.Log; + +/** + * Helper class which wraps Android's native Log utility in the Logger interface. This way + * normal DDMS output can be one of the many targets receiving and outputting logs simultaneously. + */ +public class LogWrapper implements LogNode { + + // For piping: The next node to receive Log data after this one has done its work. + private LogNode mNext; + + /** + * Returns the next LogNode in the linked list. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + + /** + * Prints data out to the console using Android's native log mechanism. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + // There actually are log methods that don't take a msg parameter. For now, + // if that's the case, just convert null to the empty string and move on. + String useMsg = msg; + if (useMsg == null) { + useMsg = ""; + } + + // If an exeption was provided, convert that exception to a usable string and attach + // it to the end of the msg method. + if (tr != null) { + msg += "\n" + Log.getStackTraceString(tr); + } + + // This is functionally identical to Log.x(tag, useMsg); + // For instance, if priority were Log.VERBOSE, this would be the same as Log.v(tag, useMsg) + Log.println(priority, tag, useMsg); + + // If this isn't the last node in the chain, move things along. + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } +} diff --git a/samples/browseable/Quiz/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java new file mode 100644 index 000000000..19967dcd4 --- /dev/null +++ b/samples/browseable/Quiz/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Simple {@link LogNode} filter, removes everything except the message. + * Useful for situations like on-screen log output where you don't want a lot of metadata displayed, + * just easy-to-read message updates as they're happening. + */ +public class MessageOnlyLogFilter implements LogNode { + + LogNode mNext; + + /** + * Takes the "next" LogNode as a parameter, to simplify chaining. + * + * @param next The next LogNode in the pipeline. + */ + public MessageOnlyLogFilter(LogNode next) { + mNext = next; + } + + public MessageOnlyLogFilter() { + } + + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + if (mNext != null) { + getNext().println(Log.NONE, null, msg, null); + } + } + + /** + * Returns the next LogNode in the chain. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + +} diff --git a/samples/browseable/Quiz/Application/src/com.example.android.quiz/Constants.java b/samples/browseable/Quiz/Application/src/com.example.android.quiz/Constants.java new file mode 100644 index 000000000..ea5c56b5e --- /dev/null +++ b/samples/browseable/Quiz/Application/src/com.example.android.quiz/Constants.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.quiz; + +/** Constants used in the companion app. */ +public final class Constants { + private Constants() { + } + + public static final String ANSWERS = "answers"; + public static final String CHOSEN_ANSWER_CORRECT = "chosen_answer_correct"; + public static final String CORRECT_ANSWER_INDEX = "correct_answer_index"; + public static final String QUESTION = "question"; + public static final String QUESTION_INDEX = "question_index"; + public static final String QUESTION_WAS_ANSWERED = "question_was_answered"; + public static final String QUESTION_WAS_DELETED = "question_was_deleted"; + + public static final String NUM_CORRECT = "num_correct"; + public static final String NUM_INCORRECT = "num_incorrect"; + public static final String NUM_SKIPPED = "num_skipped"; + + public static final String QUIZ_ENDED_PATH = "/quiz_ended"; + public static final String QUIZ_EXITED_PATH = "/quiz_exited"; + public static final String RESET_QUIZ_PATH = "/reset_quiz"; +} diff --git a/samples/browseable/Quiz/Application/src/com.example.android.quiz/JsonUtils.java b/samples/browseable/Quiz/Application/src/com.example.android.quiz/JsonUtils.java new file mode 100644 index 000000000..a0f98c12b --- /dev/null +++ b/samples/browseable/Quiz/Application/src/com.example.android.quiz/JsonUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.quiz; + +import android.content.Context; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; + +final class JsonUtils { + public static final String JSON_FIELD_QUESTIONS = "questions"; + public static final String JSON_FIELD_QUESTION = "question"; + public static final String JSON_FIELD_ANSWERS = "answers"; + public static final String JSON_FIELD_CORRECT_INDEX = "correctIndex"; + public static final int NUM_ANSWER_CHOICES = 4; + + private JsonUtils() { + } + + public static JSONObject loadJsonFile(Context context, String fileName) throws IOException, + JSONException { + InputStream is = context.getAssets().open(fileName); + int size = is.available(); + byte[] buffer = new byte[size]; + is.read(buffer); + is.close(); + String jsonString = new String(buffer); + return new JSONObject(jsonString); + } +} diff --git a/samples/browseable/Quiz/Application/src/com.example.android.quiz/MainActivity.java b/samples/browseable/Quiz/Application/src/com.example.android.quiz/MainActivity.java new file mode 100644 index 000000000..ab8e3b45b --- /dev/null +++ b/samples/browseable/Quiz/Application/src/com.example.android.quiz/MainActivity.java @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.quiz; + +import static com.example.android.quiz.Constants.ANSWERS; +import static com.example.android.quiz.Constants.CHOSEN_ANSWER_CORRECT; +import static com.example.android.quiz.Constants.CORRECT_ANSWER_INDEX; +import static com.example.android.quiz.Constants.NUM_CORRECT; +import static com.example.android.quiz.Constants.NUM_INCORRECT; +import static com.example.android.quiz.Constants.NUM_SKIPPED; +import static com.example.android.quiz.Constants.QUESTION; +import static com.example.android.quiz.Constants.QUESTION_INDEX; +import static com.example.android.quiz.Constants.QUESTION_WAS_ANSWERED; +import static com.example.android.quiz.Constants.QUESTION_WAS_DELETED; +import static com.example.android.quiz.Constants.QUIZ_ENDED_PATH; +import static com.example.android.quiz.Constants.QUIZ_EXITED_PATH; +import static com.example.android.quiz.Constants.RESET_QUIZ_PATH; + +import android.app.Activity; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RadioGroup; +import android.widget.TextView; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.data.FreezableUtils; +import com.google.android.gms.wearable.DataApi; +import com.google.android.gms.wearable.DataEvent; +import com.google.android.gms.wearable.DataEventBuffer; +import com.google.android.gms.wearable.DataItem; +import com.google.android.gms.wearable.DataItemBuffer; +import com.google.android.gms.wearable.DataMap; +import com.google.android.gms.wearable.DataMapItem; +import com.google.android.gms.wearable.MessageApi; +import com.google.android.gms.wearable.MessageEvent; +import com.google.android.gms.wearable.Node; +import com.google.android.gms.wearable.NodeApi; +import com.google.android.gms.wearable.PutDataMapRequest; +import com.google.android.gms.wearable.PutDataRequest; +import com.google.android.gms.wearable.Wearable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; + +/** + * Allows the user to create questions, which will be put as notifications on the watch's stream. + * The status of questions will be updated on the phone when the user answers them. + */ +public class MainActivity extends Activity implements DataApi.DataListener, + MessageApi.MessageListener, ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener { + + private static final String TAG = "ExampleQuizApp"; + private static final String QUIZ_JSON_FILE = "Quiz.json"; + + // Various UI components. + private EditText questionEditText; + private EditText choiceAEditText; + private EditText choiceBEditText; + private EditText choiceCEditText; + private EditText choiceDEditText; + private RadioGroup choicesRadioGroup; + private TextView quizStatus; + private LinearLayout quizButtons; + private LinearLayout questionsContainer; + private Button readQuizFromFileButton; + private Button resetQuizButton; + + private GoogleApiClient mGoogleApiClient; + private PriorityQueue mFutureQuestions; + private int mQuestionIndex = 0; + private boolean mHasQuestionBeenAsked = false; + + // Data to display in end report. + private int mNumCorrect = 0; + private int mNumIncorrect = 0; + private int mNumSkipped = 0; + + private static final Map radioIdToIndex; + + static { + Map temp = new HashMap(4); + temp.put(R.id.choice_a_radio, 0); + temp.put(R.id.choice_b_radio, 1); + temp.put(R.id.choice_c_radio, 2); + temp.put(R.id.choice_d_radio, 3); + radioIdToIndex = Collections.unmodifiableMap(temp); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + mFutureQuestions = new PriorityQueue(10); + + // Find UI components to be used later. + questionEditText = (EditText) findViewById(R.id.question_text); + choiceAEditText = (EditText) findViewById(R.id.choice_a_text); + choiceBEditText = (EditText) findViewById(R.id.choice_b_text); + choiceCEditText = (EditText) findViewById(R.id.choice_c_text); + choiceDEditText = (EditText) findViewById(R.id.choice_d_text); + choicesRadioGroup = (RadioGroup) findViewById(R.id.choices_radio_group); + quizStatus = (TextView) findViewById(R.id.quiz_status); + quizButtons = (LinearLayout) findViewById(R.id.quiz_buttons); + questionsContainer = (LinearLayout) findViewById(R.id.questions_container); + readQuizFromFileButton = (Button) findViewById(R.id.read_quiz_from_file_button); + resetQuizButton = (Button) findViewById(R.id.reset_quiz_button); + } + + @Override + protected void onStart() { + super.onStart(); + if (!mGoogleApiClient.isConnected()) { + mGoogleApiClient.connect(); + } + } + + @Override + protected void onStop() { + Wearable.DataApi.removeListener(mGoogleApiClient, this); + Wearable.MessageApi.removeListener(mGoogleApiClient, this); + + // Tell the wearable to end the quiz (counting unanswered questions as skipped), and then + // disconnect mGoogleApiClient. + DataMap dataMap = new DataMap(); + dataMap.putInt(NUM_CORRECT, mNumCorrect); + dataMap.putInt(NUM_INCORRECT, mNumIncorrect); + if (mHasQuestionBeenAsked) { + mNumSkipped += 1; + } + mNumSkipped += mFutureQuestions.size(); + dataMap.putInt(NUM_SKIPPED, mNumSkipped); + if (mNumCorrect + mNumIncorrect + mNumSkipped > 0) { + sendMessageToWearable(QUIZ_EXITED_PATH, dataMap.toByteArray()); + } + + clearQuizStatus(); + super.onStop(); + } + + @Override + public void onConnected(Bundle connectionHint) { + Wearable.DataApi.addListener(mGoogleApiClient, this); + Wearable.MessageApi.addListener(mGoogleApiClient, this); + } + + @Override + public void onConnectionSuspended(int cause) { + // Ignore + } + + @Override + public void onConnectionFailed(ConnectionResult result) { + Log.e(TAG, "Failed to connect to Google Play Services"); + } + + @Override + public void onMessageReceived(MessageEvent messageEvent) { + if (messageEvent.getPath().equals(RESET_QUIZ_PATH)) { + runOnUiThread(new Runnable() { + @Override + public void run() { + resetQuiz(null); + } + }); + } + } + + /** + * Used to ensure questions with smaller indexes come before questions with larger + * indexes. For example, question0 should come before question1. + */ + private static class Question implements Comparable { + private String question; + private int questionIndex; + private String[] answers; + private int correctAnswerIndex; + + public Question(String question, int questionIndex, String[] answers, + int correctAnswerIndex) { + this.question = question; + this.questionIndex = questionIndex; + this.answers = answers; + this.correctAnswerIndex = correctAnswerIndex; + } + + public static Question fromJson(JSONObject questionObject, int questionIndex) + throws JSONException { + String question = questionObject.getString(JsonUtils.JSON_FIELD_QUESTION); + JSONArray answersJsonArray = questionObject.getJSONArray(JsonUtils.JSON_FIELD_ANSWERS); + String[] answers = new String[JsonUtils.NUM_ANSWER_CHOICES]; + for (int j = 0; j < answersJsonArray.length(); j++) { + answers[j] = answersJsonArray.getString(j); + } + int correctIndex = questionObject.getInt(JsonUtils.JSON_FIELD_CORRECT_INDEX); + return new Question(question, questionIndex, answers, correctIndex); + } + + @Override + public int compareTo(Question that) { + return this.questionIndex - that.questionIndex; + } + + public PutDataRequest toPutDataRequest() { + PutDataMapRequest request = PutDataMapRequest.create("/question/" + questionIndex); + DataMap dataMap = request.getDataMap(); + dataMap.putString(QUESTION, question); + dataMap.putInt(QUESTION_INDEX, questionIndex); + dataMap.putStringArray(ANSWERS, answers); + dataMap.putInt(CORRECT_ANSWER_INDEX, correctAnswerIndex); + return request.asPutDataRequest(); + } + } + + /** + * Create a quiz, as defined in Quiz.json, when the user clicks on "Read quiz from file." + * @throws IOException + */ + public void readQuizFromFile(View view) throws IOException, JSONException { + clearQuizStatus(); + JSONObject jsonObject = JsonUtils.loadJsonFile(this, QUIZ_JSON_FILE); + JSONArray jsonArray = jsonObject.getJSONArray(JsonUtils.JSON_FIELD_QUESTIONS); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject questionObject = jsonArray.getJSONObject(i); + Question question = Question.fromJson(questionObject, mQuestionIndex++); + addQuestionDataItem(question); + setNewQuestionStatus(question.question); + } + } + + /** + * Adds a question (with answer choices) when user clicks on "Add Question." + */ + public void addQuestion(View view) { + // Retrieve the question and answers supplied by the user. + String question = questionEditText.getText().toString(); + String[] answers = new String[4]; + answers[0] = choiceAEditText.getText().toString(); + answers[1] = choiceBEditText.getText().toString(); + answers[2] = choiceCEditText.getText().toString(); + answers[3] = choiceDEditText.getText().toString(); + int correctAnswerIndex = radioIdToIndex.get(choicesRadioGroup.getCheckedRadioButtonId()); + + addQuestionDataItem(new Question(question, mQuestionIndex++, answers, correctAnswerIndex)); + setNewQuestionStatus(question); + + // Clear the edit boxes to let the user input a new question. + questionEditText.setText(""); + choiceAEditText.setText(""); + choiceBEditText.setText(""); + choiceCEditText.setText(""); + choiceDEditText.setText(""); + } + + /** + * Adds the questions (and answers) to the wearable's stream by creating a Data Item + * that will be received on the wearable, which will create corresponding notifications. + */ + private void addQuestionDataItem(Question question) { + if (!mHasQuestionBeenAsked) { + // Ask the question now. + Wearable.DataApi.putDataItem(mGoogleApiClient, question.toPutDataRequest()); + setHasQuestionBeenAsked(true); + } else { + // Enqueue the question to be asked in the future. + mFutureQuestions.add(question); + } + } + + /** + * Sets the question's status to be the default "unanswered." This will be updated when the + * user chooses an answer for the question on the wearable. + */ + private void setNewQuestionStatus(String question) { + quizStatus.setVisibility(View.VISIBLE); + quizButtons.setVisibility(View.VISIBLE); + LayoutInflater inflater = LayoutInflater.from(this); + View questionStatusElem = inflater.inflate(R.layout.question_status_element, null, false); + ((TextView) questionStatusElem.findViewById(R.id.question)).setText(question); + ((TextView) questionStatusElem.findViewById(R.id.status)) + .setText(R.string.question_unanswered); + questionsContainer.addView(questionStatusElem); + } + + @Override + public void onDataChanged(DataEventBuffer dataEvents) { + final List events = FreezableUtils.freezeIterable(dataEvents); + dataEvents.close(); + runOnUiThread(new Runnable() { + @Override + public void run() { + for (DataEvent event : events) { + if (event.getType() == DataEvent.TYPE_CHANGED) { + DataMap dataMap = DataMapItem.fromDataItem(event.getDataItem()) + .getDataMap(); + boolean questionWasAnswered = dataMap.getBoolean(QUESTION_WAS_ANSWERED); + boolean questionWasDeleted = dataMap.getBoolean(QUESTION_WAS_DELETED); + if (questionWasAnswered) { + // Update the answered question's status. + int questionIndex = dataMap.getInt(QUESTION_INDEX); + boolean questionCorrect = dataMap.getBoolean(CHOSEN_ANSWER_CORRECT); + updateQuestionStatus(questionIndex, questionCorrect); + askNextQuestionIfExists(); + } else if (questionWasDeleted) { + // Update the deleted question's status by marking it as left blank. + int questionIndex = dataMap.getInt(QUESTION_INDEX); + markQuestionLeftBlank(questionIndex); + askNextQuestionIfExists(); + } + } + } + } + }); + } + + /** + * Updates the given question based on whether it was answered correctly or not. + * This involves changing the question's text color and changing the status text for it. + */ + public void updateQuestionStatus(int questionIndex, boolean questionCorrect) { + LinearLayout questionStatusElement = (LinearLayout) + questionsContainer.getChildAt(questionIndex); + TextView questionText = (TextView) questionStatusElement.findViewById(R.id.question); + TextView questionStatus = (TextView) questionStatusElement.findViewById(R.id.status); + if (questionCorrect) { + questionText.setTextColor(Color.GREEN); + questionStatus.setText(R.string.question_correct); + mNumCorrect++; + } else { + questionText.setTextColor(Color.RED); + questionStatus.setText(R.string.question_incorrect); + mNumIncorrect++; + } + } + + /** + * Marks a question as "left blank" when its corresponding question notification is deleted. + */ + private void markQuestionLeftBlank(int index) { + LinearLayout questionStatusElement = (LinearLayout) questionsContainer.getChildAt(index); + if (questionStatusElement != null) { + TextView questionText = (TextView) questionStatusElement.findViewById(R.id.question); + TextView questionStatus = (TextView) questionStatusElement.findViewById(R.id.status); + if (questionStatus.getText().equals(getString(R.string.question_unanswered))) { + questionText.setTextColor(Color.YELLOW); + questionStatus.setText(R.string.question_left_blank); + mNumSkipped++; + } + } + } + + /** + * Asks the next enqueued question if it exists, otherwise ends the quiz. + */ + private void askNextQuestionIfExists() { + if (mFutureQuestions.isEmpty()) { + // Quiz has been completed - send message to wearable to display end report. + DataMap dataMap = new DataMap(); + dataMap.putInt(NUM_CORRECT, mNumCorrect); + dataMap.putInt(NUM_INCORRECT, mNumIncorrect); + dataMap.putInt(NUM_SKIPPED, mNumSkipped); + sendMessageToWearable(QUIZ_ENDED_PATH, dataMap.toByteArray()); + setHasQuestionBeenAsked(false); + } else { + // Ask next question by putting a DataItem that will be received on the wearable. + Wearable.DataApi.putDataItem(mGoogleApiClient, + mFutureQuestions.remove().toPutDataRequest()); + setHasQuestionBeenAsked(true); + } + } + + private void sendMessageToWearable(final String path, final byte[] data) { + Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).setResultCallback( + new ResultCallback() { + @Override + public void onResult(NodeApi.GetConnectedNodesResult nodes) { + for (Node node : nodes.getNodes()) { + Wearable.MessageApi.sendMessage(mGoogleApiClient, node.getId(), path, data); + } + + if (path.equals(QUIZ_EXITED_PATH) && mGoogleApiClient.isConnected()) { + mGoogleApiClient.disconnect(); + } + } + }); + } + + /** + * Resets the current quiz when Reset Quiz is pressed. + */ + public void resetQuiz(View view) { + // Reset quiz status in phone layout. + for(int i = 0; i < questionsContainer.getChildCount(); i++) { + LinearLayout questionStatusElement = (LinearLayout) questionsContainer.getChildAt(i); + TextView questionText = (TextView) questionStatusElement.findViewById(R.id.question); + TextView questionStatus = (TextView) questionStatusElement.findViewById(R.id.status); + questionText.setTextColor(Color.WHITE); + questionStatus.setText(R.string.question_unanswered); + } + // Reset data items and notifications on wearable. + if (mGoogleApiClient.isConnected()) { + Wearable.DataApi.getDataItems(mGoogleApiClient) + .setResultCallback(new ResultCallback() { + @Override + public void onResult(DataItemBuffer result) { + if (result.getStatus().isSuccess()) { + List dataItemList = FreezableUtils.freezeIterable(result); + result.close(); + resetDataItems(dataItemList); + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Reset quiz: failed to get Data Items to reset"); + } + } + result.close(); + } + }); + } else { + Log.e(TAG, "Failed to reset data items because client is disconnected from " + + "Google Play Services"); + } + setHasQuestionBeenAsked(false); + mNumCorrect = 0; + mNumIncorrect = 0; + mNumSkipped = 0; + } + + private void resetDataItems(List dataItemList) { + if (mGoogleApiClient.isConnected()) { + for (final DataItem dataItem : dataItemList) { + final Uri dataItemUri = dataItem.getUri(); + Wearable.DataApi.getDataItem(mGoogleApiClient, dataItemUri) + .setResultCallback(new ResetDataItemCallback()); + } + } else { + Log.e(TAG, "Failed to reset data items because client is disconnected from " + + "Google Play Services"); + } + } + + /** + * Callback that marks a DataItem, which represents a question, as unanswered and not deleted. + */ + private class ResetDataItemCallback implements ResultCallback { + @Override + public void onResult(DataApi.DataItemResult dataItemResult) { + if (dataItemResult.getStatus().isSuccess()) { + PutDataMapRequest request = PutDataMapRequest.createFromDataMapItem( + DataMapItem.fromDataItem(dataItemResult.getDataItem())); + DataMap dataMap = request.getDataMap(); + dataMap.putBoolean(QUESTION_WAS_ANSWERED, false); + dataMap.putBoolean(QUESTION_WAS_DELETED, false); + if (!mHasQuestionBeenAsked && dataMap.getInt(QUESTION_INDEX) == 0) { + // Ask the first question now. + Wearable.DataApi.putDataItem(mGoogleApiClient, request.asPutDataRequest()); + setHasQuestionBeenAsked(true); + } else { + // Enqueue future questions. + mFutureQuestions.add(new Question(dataMap.getString(QUESTION), + dataMap.getInt(QUESTION_INDEX), dataMap.getStringArray(ANSWERS), + dataMap.getInt(CORRECT_ANSWER_INDEX))); + } + } else { + Log.e(TAG, "Failed to reset data item " + dataItemResult.getDataItem().getUri()); + } + } + } + + /** + * Clears the current quiz when user clicks on "New Quiz." + * On this end, this involves clearing the quiz status layout and deleting all DataItems. The + * wearable will then remove any outstanding question notifications upon receiving this change. + */ + public void newQuiz(View view) { + clearQuizStatus(); + if (mGoogleApiClient.isConnected()) { + Wearable.DataApi.getDataItems(mGoogleApiClient) + .setResultCallback(new ResultCallback() { + @Override + public void onResult(DataItemBuffer result) { + if (result.getStatus().isSuccess()) { + List dataItemUriList = new ArrayList(); + for (final DataItem dataItem : result) { + dataItemUriList.add(dataItem.getUri()); + } + result.close(); + deleteDataItems(dataItemUriList); + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Clear quiz: failed to get Data Items for deletion"); + } + } + result.close(); + } + }); + } else { + Log.e(TAG, "Failed to delete data items because client is disconnected from " + + "Google Play Services"); + } + } + + /** + * Removes quiz status views (i.e. the views describing the status of each question). + */ + private void clearQuizStatus() { + questionsContainer.removeAllViews(); + quizStatus.setVisibility(View.INVISIBLE); + quizButtons.setVisibility(View.INVISIBLE); + setHasQuestionBeenAsked(false); + mFutureQuestions.clear(); + mQuestionIndex = 0; + mNumCorrect = 0; + mNumIncorrect = 0; + mNumSkipped = 0; + } + + private void deleteDataItems(List dataItemUriList) { + if (mGoogleApiClient.isConnected()) { + for (final Uri dataItemUri : dataItemUriList) { + Wearable.DataApi.deleteDataItems(mGoogleApiClient, dataItemUri) + .setResultCallback(new ResultCallback() { + @Override + public void onResult(DataApi.DeleteDataItemsResult deleteResult) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + if (deleteResult.getStatus().isSuccess()) { + Log.d(TAG, "Successfully deleted data item " + dataItemUri); + } else { + Log.d(TAG, "Failed to delete data item " + dataItemUri); + } + } + } + }); + } + } else { + Log.e(TAG, "Failed to delete data items because client is disconnected from " + + "Google Play Services"); + } + } + + private void setHasQuestionBeenAsked(boolean b) { + mHasQuestionBeenAsked = b; + // Only let user click on Reset or Read from file if they have answered all the questions. + readQuizFromFileButton.setEnabled(!mHasQuestionBeenAsked); + resetQuizButton.setEnabled(!mHasQuestionBeenAsked); + } +} diff --git a/samples/browseable/Quiz/Shared/AndroidManifest.xml b/samples/browseable/Quiz/Shared/AndroidManifest.xml new file mode 100644 index 000000000..0d7b8a69c --- /dev/null +++ b/samples/browseable/Quiz/Shared/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/samples/browseable/Quiz/Shared/res/values/strings.xml b/samples/browseable/Quiz/Shared/res/values/strings.xml new file mode 100644 index 000000000..0f2bb9075 --- /dev/null +++ b/samples/browseable/Quiz/Shared/res/values/strings.xml @@ -0,0 +1,18 @@ + + + + Shared + diff --git a/samples/browseable/Quiz/Wearable/AndroidManifest.xml b/samples/browseable/Quiz/Wearable/AndroidManifest.xml new file mode 100644 index 000000000..7954e3271 --- /dev/null +++ b/samples/browseable/Quiz/Wearable/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_a.png b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_a.png new file mode 100644 index 000000000..de18ce11a Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_a.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_b.png b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_b.png new file mode 100644 index 000000000..3cdfe9750 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_b.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_c.png b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_c.png new file mode 100644 index 000000000..f0ed2ef99 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_c.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_d.png b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_d.png new file mode 100644 index 000000000..c158d2953 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_choice_d.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_launcher.png b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_launcher.png new file mode 100755 index 000000000..91a8cff95 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_unknown_choice.png b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_unknown_choice.png new file mode 100644 index 000000000..9aed51728 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-hdpi/ic_unknown_choice.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_a.png b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_a.png new file mode 100644 index 000000000..57459365c Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_a.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_b.png b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_b.png new file mode 100644 index 000000000..958b92e31 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_b.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_c.png b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_c.png new file mode 100644 index 000000000..9fcfab754 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_c.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_d.png b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_d.png new file mode 100644 index 000000000..821cadb78 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_choice_d.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_launcher.png b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_launcher.png new file mode 100755 index 000000000..728ee6d73 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_unknown_choice.png b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_unknown_choice.png new file mode 100644 index 000000000..b8030ef08 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-mdpi/ic_unknown_choice.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_a.png b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_a.png new file mode 100644 index 000000000..3dba96f32 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_a.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_b.png b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_b.png new file mode 100644 index 000000000..9ca3c85f2 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_b.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_c.png b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_c.png new file mode 100644 index 000000000..b84b3b76d Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_c.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_d.png b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_d.png new file mode 100644 index 000000000..185e91ec8 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_choice_d.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_launcher.png b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_launcher.png new file mode 100755 index 000000000..64a585453 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_unknown_choice.png b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_unknown_choice.png new file mode 100644 index 000000000..57838d152 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-xhdpi/ic_unknown_choice.png differ diff --git a/samples/browseable/Quiz/Wearable/res/drawable-xxhdpi/ic_launcher.png b/samples/browseable/Quiz/Wearable/res/drawable-xxhdpi/ic_launcher.png new file mode 100755 index 000000000..86a395bb6 Binary files /dev/null and b/samples/browseable/Quiz/Wearable/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/samples/browseable/Quiz/Wearable/res/values/colors.xml b/samples/browseable/Quiz/Wearable/res/values/colors.xml new file mode 100644 index 000000000..b10adafa3 --- /dev/null +++ b/samples/browseable/Quiz/Wearable/res/values/colors.xml @@ -0,0 +1,21 @@ + + + + + #009900 + #800000 + #FF9900 + diff --git a/samples/browseable/Quiz/Wearable/res/values/strings.xml b/samples/browseable/Quiz/Wearable/res/values/strings.xml new file mode 100644 index 000000000..313ee4c29 --- /dev/null +++ b/samples/browseable/Quiz/Wearable/res/values/strings.xml @@ -0,0 +1,29 @@ + + + + + + Quiz Sample Wearable App + + Question %d + + Quiz Report + correct + incorrect + skipped + Reset Quiz + + diff --git a/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/Constants.java b/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/Constants.java new file mode 100644 index 000000000..8218ad91d --- /dev/null +++ b/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/Constants.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.quiz; + +/** Constants used in the wearable app. */ +public final class Constants { + private Constants() { + } + + public static final String ANSWERS = "answers"; + public static final String CHOSEN_ANSWER_CORRECT = "chosen_answer_correct"; + public static final String CORRECT_ANSWER_INDEX = "correct_answer_index"; + public static final String QUESTION = "question"; + public static final String QUESTION_INDEX = "question_index"; + public static final String QUESTION_WAS_ANSWERED = "question_was_answered"; + public static final String QUESTION_WAS_DELETED = "question_was_deleted"; + + public static final String NUM_CORRECT = "num_correct"; + public static final String NUM_INCORRECT = "num_incorrect"; + public static final String NUM_SKIPPED = "num_skipped"; + + public static final String QUIZ_ENDED_PATH = "/quiz_ended"; + public static final String QUIZ_EXITED_PATH = "/quiz_exited"; + public static final String RESET_QUIZ_PATH = "/reset_quiz"; + + public static final int CONNECT_TIMEOUT_MS = 100; +} diff --git a/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/DeleteQuestionService.java b/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/DeleteQuestionService.java new file mode 100644 index 000000000..38b5e4a94 --- /dev/null +++ b/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/DeleteQuestionService.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.quiz; + +import android.app.IntentService; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.DataApi; +import com.google.android.gms.wearable.DataMap; +import com.google.android.gms.wearable.DataMapItem; +import com.google.android.gms.wearable.PutDataMapRequest; +import com.google.android.gms.wearable.PutDataRequest; +import com.google.android.gms.wearable.Wearable; + +import java.util.concurrent.TimeUnit; + +import static com.example.android.quiz.Constants.CONNECT_TIMEOUT_MS; +import static com.example.android.quiz.Constants.QUESTION_WAS_DELETED; + +/** + * Used to update quiz status on the phone when user dismisses a question on the watch. + */ +public class DeleteQuestionService extends IntentService + implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { + + private static final String TAG = "DeleteQuestionReceiver"; + + private GoogleApiClient mGoogleApiClient; + + public DeleteQuestionService() { + super(DeleteQuestionService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + } + + @Override + public void onHandleIntent(Intent intent) { + mGoogleApiClient.blockingConnect(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS); + Uri dataItemUri = intent.getData(); + if (!mGoogleApiClient.isConnected()) { + Log.e(TAG, "Failed to update data item " + dataItemUri + + " because client is disconnected from Google Play Services"); + return; + } + DataApi.DataItemResult dataItemResult = Wearable.DataApi.getDataItem( + mGoogleApiClient, dataItemUri).await(); + PutDataMapRequest putDataMapRequest = PutDataMapRequest + .createFromDataMapItem(DataMapItem.fromDataItem(dataItemResult.getDataItem())); + DataMap dataMap = putDataMapRequest.getDataMap(); + dataMap.putBoolean(QUESTION_WAS_DELETED, true); + PutDataRequest request = putDataMapRequest.asPutDataRequest(); + Wearable.DataApi.putDataItem(mGoogleApiClient, request).await(); + mGoogleApiClient.disconnect(); + } + + @Override + public void onConnected(Bundle bundle) { + } + + @Override + public void onConnectionSuspended(int i) { + } + + @Override + public void onConnectionFailed(ConnectionResult connectionResult) { + } +} diff --git a/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/QuizListenerService.java b/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/QuizListenerService.java new file mode 100644 index 000000000..3226f9b42 --- /dev/null +++ b/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/QuizListenerService.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.quiz; + +import static com.example.android.quiz.Constants.ANSWERS; +import static com.example.android.quiz.Constants.CONNECT_TIMEOUT_MS; +import static com.example.android.quiz.Constants.CORRECT_ANSWER_INDEX; +import static com.example.android.quiz.Constants.NUM_CORRECT; +import static com.example.android.quiz.Constants.NUM_INCORRECT; +import static com.example.android.quiz.Constants.NUM_SKIPPED; +import static com.example.android.quiz.Constants.QUESTION; +import static com.example.android.quiz.Constants.QUESTION_INDEX; +import static com.example.android.quiz.Constants.QUESTION_WAS_ANSWERED; +import static com.example.android.quiz.Constants.QUESTION_WAS_DELETED; +import static com.example.android.quiz.Constants.QUIZ_ENDED_PATH; +import static com.example.android.quiz.Constants.QUIZ_EXITED_PATH; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; +import android.text.SpannableStringBuilder; +import android.text.style.ForegroundColorSpan; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.data.FreezableUtils; +import com.google.android.gms.wearable.DataEvent; +import com.google.android.gms.wearable.DataEventBuffer; +import com.google.android.gms.wearable.DataItem; +import com.google.android.gms.wearable.DataMap; +import com.google.android.gms.wearable.DataMapItem; +import com.google.android.gms.wearable.MessageEvent; +import com.google.android.gms.wearable.Wearable; +import com.google.android.gms.wearable.WearableListenerService; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Listens to changes in DataItems, which represent quiz questions. + * If a new question is created, this builds a new notification for it. + * Otherwise, if a question is deleted, this cancels the corresponding notification. + * + * When the quiz ends, this listener receives a message telling it to create an end-of-quiz report. + */ +public class QuizListenerService extends WearableListenerService { + private static final String TAG = "QuizSample"; + private static final int QUIZ_REPORT_NOTIF_ID = -1; // Never used by question notifications. + private static final Map questionNumToDrawableId; + + static { + Map temp = new HashMap(4); + temp.put(0, R.drawable.ic_choice_a); + temp.put(1, R.drawable.ic_choice_b); + temp.put(2, R.drawable.ic_choice_c); + temp.put(3, R.drawable.ic_choice_d); + questionNumToDrawableId = Collections.unmodifiableMap(temp); + } + + @Override + public void onDataChanged(DataEventBuffer dataEvents) { + final List events = FreezableUtils.freezeIterable(dataEvents); + dataEvents.close(); + + GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .build(); + + ConnectionResult connectionResult = googleApiClient.blockingConnect(CONNECT_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + if (!connectionResult.isSuccess()) { + Log.e(TAG, "QuizListenerService failed to connect to GoogleApiClient."); + return; + } + + for (DataEvent event : events) { + if (event.getType() == DataEvent.TYPE_CHANGED) { + DataItem dataItem = event.getDataItem(); + DataMap dataMap = DataMapItem.fromDataItem(dataItem).getDataMap(); + if (dataMap.getBoolean(QUESTION_WAS_ANSWERED) + || dataMap.getBoolean(QUESTION_WAS_DELETED)) { + // Ignore the change in data; it is used in MainActivity to update + // the question's status (i.e. was the answer right or wrong or left blank). + continue; + } + String question = dataMap.getString(QUESTION); + int questionIndex = dataMap.getInt(QUESTION_INDEX); + int questionNum = questionIndex + 1; + String[] answers = dataMap.getStringArray(ANSWERS); + int correctAnswerIndex = dataMap.getInt(CORRECT_ANSWER_INDEX); + Intent deleteOperation = new Intent(this, DeleteQuestionService.class); + deleteOperation.setData(dataItem.getUri()); + PendingIntent deleteIntent = PendingIntent.getService(this, 0, + deleteOperation, PendingIntent.FLAG_UPDATE_CURRENT); + // First page of notification contains question as Big Text. + Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle() + .setBigContentTitle(getString(R.string.question, questionNum)) + .bigText(question); + Notification.Builder builder = new Notification.Builder(this) + .setStyle(bigTextStyle) + .setSmallIcon(R.drawable.ic_launcher) + .setLocalOnly(true) + .setDeleteIntent(deleteIntent); + + // Add answers as actions. + Notification.WearableExtender wearableOptions = new Notification.WearableExtender(); + for (int i = 0; i < answers.length; i++) { + Notification answerPage = new Notification.Builder(this) + .setContentTitle(question) + .setContentText(answers[i]) + .extend(new Notification.WearableExtender() + .setContentAction(i)) + .build(); + + boolean correct = (i == correctAnswerIndex); + Intent updateOperation = new Intent(this, UpdateQuestionService.class); + // Give each intent a unique action. + updateOperation.setAction("question_" + questionIndex + "_answer_" + i); + updateOperation.setData(dataItem.getUri()); + updateOperation.putExtra(UpdateQuestionService.EXTRA_QUESTION_INDEX, + questionIndex); + updateOperation.putExtra(UpdateQuestionService.EXTRA_QUESTION_CORRECT, correct); + PendingIntent updateIntent = PendingIntent.getService(this, 0, updateOperation, + PendingIntent.FLAG_UPDATE_CURRENT); + Notification.Action action = new Notification.Action.Builder( + questionNumToDrawableId.get(i), null, updateIntent) + .build(); + wearableOptions.addAction(action).addPage(answerPage); + } + builder.extend(wearableOptions); + Notification notification = builder.build(); + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) + .notify(questionIndex, notification); + } else if (event.getType() == DataEvent.TYPE_DELETED) { + Uri uri = event.getDataItem().getUri(); + // URI's are of the form "/question/0", "/question/1" etc. + // We use the question index as the notification id. + int notificationId = Integer.parseInt(uri.getLastPathSegment()); + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) + .cancel(notificationId); + } + // Delete the quiz report, if it exists. + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) + .cancel(QUIZ_REPORT_NOTIF_ID); + } + googleApiClient.disconnect(); + } + + @Override + public void onMessageReceived(MessageEvent messageEvent) { + String path = messageEvent.getPath(); + if (path.equals(QUIZ_EXITED_PATH)) { + // Remove any lingering question notifications. + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancelAll(); + } + if (path.equals(QUIZ_ENDED_PATH) || path.equals(QUIZ_EXITED_PATH)) { + // Quiz ended - display overall results. + DataMap dataMap = DataMap.fromByteArray(messageEvent.getData()); + int numCorrect = dataMap.getInt(NUM_CORRECT); + int numIncorrect = dataMap.getInt(NUM_INCORRECT); + int numSkipped = dataMap.getInt(NUM_SKIPPED); + + Notification.Builder builder = new Notification.Builder(this) + .setContentTitle(getString(R.string.quiz_report)) + .setSmallIcon(R.drawable.ic_launcher) + .setLocalOnly(true); + SpannableStringBuilder quizReportText = new SpannableStringBuilder(); + appendColored(quizReportText, String.valueOf(numCorrect), R.color.dark_green); + quizReportText.append(" " + getString(R.string.correct) + "\n"); + appendColored(quizReportText, String.valueOf(numIncorrect), R.color.dark_red); + quizReportText.append(" " + getString(R.string.incorrect) + "\n"); + appendColored(quizReportText, String.valueOf(numSkipped), R.color.dark_yellow); + quizReportText.append(" " + getString(R.string.skipped) + "\n"); + + builder.setContentText(quizReportText); + if (!path.equals(QUIZ_EXITED_PATH)) { + // Don't add reset option if user exited quiz (there might not be a quiz to reset!). + builder.addAction(R.drawable.ic_launcher, + getString(R.string.reset_quiz), getResetQuizPendingIntent()); + } + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) + .notify(QUIZ_REPORT_NOTIF_ID, builder.build()); + } + } + + private void appendColored(SpannableStringBuilder builder, String text, int colorResId) { + builder.append(text).setSpan(new ForegroundColorSpan(getResources().getColor(colorResId)), + builder.length() - text.length(), builder.length(), 0); + } + + /** + * Returns a PendingIntent that will send a message to the phone to reset the quiz when fired. + */ + private PendingIntent getResetQuizPendingIntent() { + Intent intent = new Intent(QuizReportActionService.ACTION_RESET_QUIZ) + .setClass(this, QuizReportActionService.class); + return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } +} diff --git a/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/QuizReportActionService.java b/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/QuizReportActionService.java new file mode 100644 index 000000000..4ca55be83 --- /dev/null +++ b/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/QuizReportActionService.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.quiz; + +import android.app.IntentService; +import android.content.Intent; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.Node; +import com.google.android.gms.wearable.NodeApi; +import com.google.android.gms.wearable.Wearable; + +import java.util.concurrent.TimeUnit; + +import static com.example.android.quiz.Constants.CONNECT_TIMEOUT_MS; +import static com.example.android.quiz.Constants.RESET_QUIZ_PATH; + +/** + * Service to reset the quiz (by sending a message to the phone) when the Reset Quiz + * action on the Quiz Report is selected. + */ +public class QuizReportActionService extends IntentService { + public static final String ACTION_RESET_QUIZ = "com.example.android.quiz.RESET_QUIZ"; + + private static final String TAG = "QuizReportActionReceiver"; + + public QuizReportActionService() { + super(QuizReportActionService.class.getSimpleName()); + } + + @Override + public void onHandleIntent(Intent intent) { + if (intent.getAction().equals(ACTION_RESET_QUIZ)) { + GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .build(); + ConnectionResult result = googleApiClient.blockingConnect(CONNECT_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + if (!result.isSuccess()) { + Log.e(TAG, "QuizListenerService failed to connect to GoogleApiClient."); + return; + } + NodeApi.GetConnectedNodesResult nodes = + Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); + for (Node node : nodes.getNodes()) { + Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), RESET_QUIZ_PATH, + new byte[0]); + } + } + } +} diff --git a/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/UpdateQuestionService.java b/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/UpdateQuestionService.java new file mode 100644 index 000000000..671ecad2c --- /dev/null +++ b/samples/browseable/Quiz/Wearable/src/com.example.android.quiz/UpdateQuestionService.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.quiz; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.DataApi; +import com.google.android.gms.wearable.DataMap; +import com.google.android.gms.wearable.DataMapItem; +import com.google.android.gms.wearable.PutDataMapRequest; +import com.google.android.gms.wearable.PutDataRequest; +import com.google.android.gms.wearable.Wearable; + +import java.util.concurrent.TimeUnit; + +import static com.example.android.quiz.Constants.CHOSEN_ANSWER_CORRECT; +import static com.example.android.quiz.Constants.QUESTION_INDEX; +import static com.example.android.quiz.Constants.QUESTION_WAS_ANSWERED; + +/** + * Updates quiz status on the phone when user selects an answer to a question on the watch. + */ +public class UpdateQuestionService extends IntentService + implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { + public static final String EXTRA_QUESTION_CORRECT = "extra_question_correct"; + public static final String EXTRA_QUESTION_INDEX = "extra_question_index"; + + private static final long TIME_OUT_MS = 100; + private static final String TAG = "UpdateQuestionService"; + + private GoogleApiClient mGoogleApiClient; + + public UpdateQuestionService() { + super(UpdateQuestionService.class.getSimpleName()); + } + + @Override + public void onCreate() { + super.onCreate(); + mGoogleApiClient = new GoogleApiClient.Builder(this) + .addApi(Wearable.API) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .build(); + } + + @Override + protected void onHandleIntent(Intent intent) { + mGoogleApiClient.blockingConnect(TIME_OUT_MS, TimeUnit.MILLISECONDS); + Uri dataItemUri = intent.getData(); + if (!mGoogleApiClient.isConnected()) { + Log.e(TAG, "Failed to update data item " + dataItemUri + + " because client is disconnected from Google Play Services"); + return; + } + DataApi.DataItemResult dataItemResult = Wearable.DataApi.getDataItem( + mGoogleApiClient, dataItemUri).await(); + PutDataMapRequest putDataMapRequest = PutDataMapRequest + .createFromDataMapItem(DataMapItem.fromDataItem(dataItemResult.getDataItem())); + DataMap dataMap = putDataMapRequest.getDataMap(); + + // Update quiz status variables, which will be reflected on the phone. + int questionIndex = intent.getIntExtra(EXTRA_QUESTION_INDEX, -1); + boolean chosenAnswerCorrect = intent.getBooleanExtra(EXTRA_QUESTION_CORRECT, false); + dataMap.putInt(QUESTION_INDEX, questionIndex); + dataMap.putBoolean(CHOSEN_ANSWER_CORRECT, chosenAnswerCorrect); + dataMap.putBoolean(QUESTION_WAS_ANSWERED, true); + PutDataRequest request = putDataMapRequest.asPutDataRequest(); + Wearable.DataApi.putDataItem(mGoogleApiClient, request).await(); + + // Remove this question notification. + ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancel(questionIndex); + mGoogleApiClient.disconnect(); + } + + @Override + public void onConnected(Bundle connectionHint) { + } + + @Override + public void onConnectionSuspended(int cause) { + } + + @Override + public void onConnectionFailed(ConnectionResult result) { + } +} diff --git a/samples/browseable/Quiz/_index.jd b/samples/browseable/Quiz/_index.jd new file mode 100644 index 000000000..3c1a1daa1 --- /dev/null +++ b/samples/browseable/Quiz/_index.jd @@ -0,0 +1,15 @@ +page.tags="Quiz" +sample.group=Wearable +@jd:body + +

+ + This sample uses Google Play Services Wearable Data APIs to communicate between + applications on a phone and a paired wearable device. Users can create quiz questions on the phone, + each of which has an associated DataItem. These DataItems are then received on the wearable, which + displays them as notifications. Each notification contains the question as the first page, followed + by answers as actions. When an answer is selected, the corresponding question\'s DataItem is updated, + which allows the phone application to update the status of the question (i.e. did the user answer it + correctly or not) and prompt the next question. + +

diff --git a/samples/browseable/RecipeAssistant/Application/AndroidManifest.xml b/samples/browseable/RecipeAssistant/Application/AndroidManifest.xml new file mode 100644 index 000000000..db13ed99e --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/RecipeAssistant/Application/res/drawable-hdpi/ic_noimage.png b/samples/browseable/RecipeAssistant/Application/res/drawable-hdpi/ic_noimage.png new file mode 100644 index 000000000..7bba7ab76 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/drawable-hdpi/ic_noimage.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/drawable-hdpi/tile.9.png b/samples/browseable/RecipeAssistant/Application/res/drawable-hdpi/tile.9.png new file mode 100644 index 000000000..135862883 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/drawable-hdpi/tile.9.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/drawable-mdpi/ic_noimage.png b/samples/browseable/RecipeAssistant/Application/res/drawable-mdpi/ic_noimage.png new file mode 100644 index 000000000..a5ad26f64 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/drawable-mdpi/ic_noimage.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/drawable-xhdpi/ic_noimage.png b/samples/browseable/RecipeAssistant/Application/res/drawable-xhdpi/ic_noimage.png new file mode 100644 index 000000000..8b631d121 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/drawable-xhdpi/ic_noimage.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/layout/activity_main.xml b/samples/browseable/RecipeAssistant/Application/res/layout/activity_main.xml new file mode 100755 index 000000000..be1aa49d9 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/res/layout/activity_main.xml @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/samples/browseable/RecipeAssistant/Application/res/layout/list_item.xml b/samples/browseable/RecipeAssistant/Application/res/layout/list_item.xml new file mode 100644 index 000000000..756880c8f --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/res/layout/list_item.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + diff --git a/samples/browseable/RecipeAssistant/Application/res/layout/recipe.xml b/samples/browseable/RecipeAssistant/Application/res/layout/recipe.xml new file mode 100644 index 000000000..3bb20b86b --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/res/layout/recipe.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/RecipeAssistant/Application/res/layout/step_item.xml b/samples/browseable/RecipeAssistant/Application/res/layout/step_item.xml new file mode 100644 index 000000000..faa5ac5c8 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/res/layout/step_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + diff --git a/samples/browseable/RecipeAssistant/Application/res/menu/main.xml b/samples/browseable/RecipeAssistant/Application/res/menu/main.xml new file mode 100644 index 000000000..b392472f8 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/res/menu/main.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/samples/browseable/RecipeAssistant/Application/res/mipmap-hdpi/ic_app_recipe.png b/samples/browseable/RecipeAssistant/Application/res/mipmap-hdpi/ic_app_recipe.png new file mode 100644 index 000000000..8ceb8696d Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/mipmap-hdpi/ic_app_recipe.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/mipmap-hdpi/ic_notification_recipe.png b/samples/browseable/RecipeAssistant/Application/res/mipmap-hdpi/ic_notification_recipe.png new file mode 100644 index 000000000..844d8ede8 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/mipmap-hdpi/ic_notification_recipe.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/mipmap-mdpi/ic_app_recipe.png b/samples/browseable/RecipeAssistant/Application/res/mipmap-mdpi/ic_app_recipe.png new file mode 100644 index 000000000..b884789c5 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/mipmap-mdpi/ic_app_recipe.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/mipmap-mdpi/ic_notification_recipe.png b/samples/browseable/RecipeAssistant/Application/res/mipmap-mdpi/ic_notification_recipe.png new file mode 100644 index 000000000..3f3f58cde Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/mipmap-mdpi/ic_notification_recipe.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/mipmap-xhdpi/ic_app_recipe.png b/samples/browseable/RecipeAssistant/Application/res/mipmap-xhdpi/ic_app_recipe.png new file mode 100644 index 000000000..2a27c3283 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/mipmap-xhdpi/ic_app_recipe.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/mipmap-xhdpi/ic_notification_recipe.png b/samples/browseable/RecipeAssistant/Application/res/mipmap-xhdpi/ic_notification_recipe.png new file mode 100644 index 000000000..5a99b7c71 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/mipmap-xhdpi/ic_notification_recipe.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/mipmap-xxhdpi/ic_app_recipe.png b/samples/browseable/RecipeAssistant/Application/res/mipmap-xxhdpi/ic_app_recipe.png new file mode 100644 index 000000000..b10c77068 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/mipmap-xxhdpi/ic_app_recipe.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/mipmap-xxhdpi/ic_notification_recipe.png b/samples/browseable/RecipeAssistant/Application/res/mipmap-xxhdpi/ic_notification_recipe.png new file mode 100644 index 000000000..799726d05 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/mipmap-xxhdpi/ic_notification_recipe.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/mipmap-xxxhdpi/ic_app_recipe.png b/samples/browseable/RecipeAssistant/Application/res/mipmap-xxxhdpi/ic_app_recipe.png new file mode 100644 index 000000000..606f07f77 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/mipmap-xxxhdpi/ic_app_recipe.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/mipmap-xxxhdpi/ic_notification_recipe.png b/samples/browseable/RecipeAssistant/Application/res/mipmap-xxxhdpi/ic_notification_recipe.png new file mode 100644 index 000000000..30e28a884 Binary files /dev/null and b/samples/browseable/RecipeAssistant/Application/res/mipmap-xxxhdpi/ic_notification_recipe.png differ diff --git a/samples/browseable/RecipeAssistant/Application/res/values-sw600dp/template-dimens.xml b/samples/browseable/RecipeAssistant/Application/res/values-sw600dp/template-dimens.xml new file mode 100644 index 000000000..22074a2bd --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/res/values-sw600dp/template-dimens.xml @@ -0,0 +1,24 @@ + + + + + + + @dimen/margin_huge + @dimen/margin_medium + + diff --git a/samples/browseable/RecipeAssistant/Application/res/values-sw600dp/template-styles.xml b/samples/browseable/RecipeAssistant/Application/res/values-sw600dp/template-styles.xml new file mode 100644 index 000000000..03d197418 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/res/values-sw600dp/template-styles.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/samples/browseable/RecipeAssistant/Application/res/values-v11/template-styles.xml b/samples/browseable/RecipeAssistant/Application/res/values-v11/template-styles.xml new file mode 100644 index 000000000..8c1ea66f2 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/res/values-v11/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/samples/browseable/RecipeAssistant/Application/res/values/template-dimens.xml b/samples/browseable/RecipeAssistant/Application/res/values/template-dimens.xml new file mode 100644 index 000000000..39e710b5c --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/res/values/template-dimens.xml @@ -0,0 +1,32 @@ + + + + + + + 4dp + 8dp + 16dp + 32dp + 64dp + + + + @dimen/margin_medium + @dimen/margin_medium + + diff --git a/samples/browseable/RecipeAssistant/Application/res/values/template-styles.xml b/samples/browseable/RecipeAssistant/Application/res/values/template-styles.xml new file mode 100644 index 000000000..6e7d593dd --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/res/values/template-styles.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/activities/SampleActivityBase.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/activities/SampleActivityBase.java new file mode 100644 index 000000000..3228927b7 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/activities/SampleActivityBase.java @@ -0,0 +1,52 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.example.android.common.activities; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogWrapper; + +/** + * Base launcher activity, to handle most of the common plumbing for samples. + */ +public class SampleActivityBase extends FragmentActivity { + + public static final String TAG = "SampleActivityBase"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void onStart() { + super.onStart(); + initializeLogging(); + } + + /** Set up targets to receive log data */ + public void initializeLogging() { + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + // Wraps Android's native log framework + LogWrapper logWrapper = new LogWrapper(); + Log.setLogNode(logWrapper); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/Log.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/Log.java new file mode 100644 index 000000000..17503c568 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/Log.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Helper class for a list (or tree) of LoggerNodes. + * + *

When this is set as the head of the list, + * an instance of it can function as a drop-in replacement for {@link android.util.Log}. + * Most of the methods in this class server only to map a method call in Log to its equivalent + * in LogNode.

+ */ +public class Log { + // Grabbing the native values from Android's native logging facilities, + // to make for easy migration and interop. + public static final int NONE = -1; + public static final int VERBOSE = android.util.Log.VERBOSE; + public static final int DEBUG = android.util.Log.DEBUG; + public static final int INFO = android.util.Log.INFO; + public static final int WARN = android.util.Log.WARN; + public static final int ERROR = android.util.Log.ERROR; + public static final int ASSERT = android.util.Log.ASSERT; + + // Stores the beginning of the LogNode topology. + private static LogNode mLogNode; + + /** + * Returns the next LogNode in the linked list. + */ + public static LogNode getLogNode() { + return mLogNode; + } + + /** + * Sets the LogNode data will be sent to. + */ + public static void setLogNode(LogNode node) { + mLogNode = node; + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void println(int priority, String tag, String msg, Throwable tr) { + if (mLogNode != null) { + mLogNode.println(priority, tag, msg, tr); + } + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + */ + public static void println(int priority, String tag, String msg) { + println(priority, tag, msg, null); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void v(String tag, String msg, Throwable tr) { + println(VERBOSE, tag, msg, tr); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void v(String tag, String msg) { + v(tag, msg, null); + } + + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void d(String tag, String msg, Throwable tr) { + println(DEBUG, tag, msg, tr); + } + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void d(String tag, String msg) { + d(tag, msg, null); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void i(String tag, String msg, Throwable tr) { + println(INFO, tag, msg, tr); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void i(String tag, String msg) { + i(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, String msg, Throwable tr) { + println(WARN, tag, msg, tr); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void w(String tag, String msg) { + w(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, Throwable tr) { + w(tag, null, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void e(String tag, String msg, Throwable tr) { + println(ERROR, tag, msg, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void e(String tag, String msg) { + e(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, String msg, Throwable tr) { + println(ASSERT, tag, msg, tr); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void wtf(String tag, String msg) { + wtf(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, Throwable tr) { + wtf(tag, null, tr); + } +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogFragment.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogFragment.java new file mode 100644 index 000000000..b302acd4b --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogFragment.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.logger; + +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +/** + * Simple fraggment which contains a LogView and uses is to output log data it receives + * through the LogNode interface. + */ +public class LogFragment extends Fragment { + + private LogView mLogView; + private ScrollView mScrollView; + + public LogFragment() {} + + public View inflateViews() { + mScrollView = new ScrollView(getActivity()); + ViewGroup.LayoutParams scrollParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mScrollView.setLayoutParams(scrollParams); + + mLogView = new LogView(getActivity()); + ViewGroup.LayoutParams logParams = new ViewGroup.LayoutParams(scrollParams); + logParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + mLogView.setLayoutParams(logParams); + mLogView.setClickable(true); + mLogView.setFocusable(true); + mLogView.setTypeface(Typeface.MONOSPACE); + + // Want to set padding as 16 dips, setPadding takes pixels. Hooray math! + int paddingDips = 16; + double scale = getResources().getDisplayMetrics().density; + int paddingPixels = (int) ((paddingDips * (scale)) + .5); + mLogView.setPadding(paddingPixels, paddingPixels, paddingPixels, paddingPixels); + mLogView.setCompoundDrawablePadding(paddingPixels); + + mLogView.setGravity(Gravity.BOTTOM); + mLogView.setTextAppearance(getActivity(), android.R.style.TextAppearance_Holo_Medium); + + mScrollView.addView(mLogView); + return mScrollView; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View result = inflateViews(); + + mLogView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + return result; + } + + public LogView getLogView() { + return mLogView; + } +} \ No newline at end of file diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogNode.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogNode.java new file mode 100644 index 000000000..bc37cabc0 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogNode.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Basic interface for a logging system that can output to one or more targets. + * Note that in addition to classes that will output these logs in some format, + * one can also implement this interface over a filter and insert that in the chain, + * such that no targets further down see certain data, or see manipulated forms of the data. + * You could, for instance, write a "ToHtmlLoggerNode" that just converted all the log data + * it received to HTML and sent it along to the next node in the chain, without printing it + * anywhere. + */ +public interface LogNode { + + /** + * Instructs first LogNode in the list to print the log data provided. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public void println(int priority, String tag, String msg, Throwable tr); + +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogView.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogView.java new file mode 100644 index 000000000..c01542b91 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.app.Activity; +import android.content.Context; +import android.util.*; +import android.widget.TextView; + +/** Simple TextView which is used to output log data received through the LogNode interface. +*/ +public class LogView extends TextView implements LogNode { + + public LogView(Context context) { + super(context); + } + + public LogView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LogView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Formats the log data and prints it out to the LogView. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + + + String priorityStr = null; + + // For the purposes of this View, we want to print the priority as readable text. + switch(priority) { + case android.util.Log.VERBOSE: + priorityStr = "VERBOSE"; + break; + case android.util.Log.DEBUG: + priorityStr = "DEBUG"; + break; + case android.util.Log.INFO: + priorityStr = "INFO"; + break; + case android.util.Log.WARN: + priorityStr = "WARN"; + break; + case android.util.Log.ERROR: + priorityStr = "ERROR"; + break; + case android.util.Log.ASSERT: + priorityStr = "ASSERT"; + break; + default: + break; + } + + // Handily, the Log class has a facility for converting a stack trace into a usable string. + String exceptionStr = null; + if (tr != null) { + exceptionStr = android.util.Log.getStackTraceString(tr); + } + + // Take the priority, tag, message, and exception, and concatenate as necessary + // into one usable line of text. + final StringBuilder outputBuilder = new StringBuilder(); + + String delimiter = "\t"; + appendIfNotNull(outputBuilder, priorityStr, delimiter); + appendIfNotNull(outputBuilder, tag, delimiter); + appendIfNotNull(outputBuilder, msg, delimiter); + appendIfNotNull(outputBuilder, exceptionStr, delimiter); + + // In case this was originally called from an AsyncTask or some other off-UI thread, + // make sure the update occurs within the UI thread. + ((Activity) getContext()).runOnUiThread( (new Thread(new Runnable() { + @Override + public void run() { + // Display the text we just generated within the LogView. + appendToLog(outputBuilder.toString()); + } + }))); + + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } + + public LogNode getNext() { + return mNext; + } + + public void setNext(LogNode node) { + mNext = node; + } + + /** Takes a string and adds to it, with a separator, if the bit to be added isn't null. Since + * the logger takes so many arguments that might be null, this method helps cut out some of the + * agonizing tedium of writing the same 3 lines over and over. + * @param source StringBuilder containing the text to append to. + * @param addStr The String to append + * @param delimiter The String to separate the source and appended strings. A tab or comma, + * for instance. + * @return The fully concatenated String as a StringBuilder + */ + private StringBuilder appendIfNotNull(StringBuilder source, String addStr, String delimiter) { + if (addStr != null) { + if (addStr.length() == 0) { + delimiter = ""; + } + + return source.append(addStr).append(delimiter); + } + return source; + } + + // The next LogNode in the chain. + LogNode mNext; + + /** Outputs the string as a new line of log data in the LogView. */ + public void appendToLog(String s) { + append("\n" + s); + } + + +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogWrapper.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogWrapper.java new file mode 100644 index 000000000..16a9e7ba2 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/LogWrapper.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.util.Log; + +/** + * Helper class which wraps Android's native Log utility in the Logger interface. This way + * normal DDMS output can be one of the many targets receiving and outputting logs simultaneously. + */ +public class LogWrapper implements LogNode { + + // For piping: The next node to receive Log data after this one has done its work. + private LogNode mNext; + + /** + * Returns the next LogNode in the linked list. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + + /** + * Prints data out to the console using Android's native log mechanism. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + // There actually are log methods that don't take a msg parameter. For now, + // if that's the case, just convert null to the empty string and move on. + String useMsg = msg; + if (useMsg == null) { + useMsg = ""; + } + + // If an exeption was provided, convert that exception to a usable string and attach + // it to the end of the msg method. + if (tr != null) { + msg += "\n" + Log.getStackTraceString(tr); + } + + // This is functionally identical to Log.x(tag, useMsg); + // For instance, if priority were Log.VERBOSE, this would be the same as Log.v(tag, useMsg) + Log.println(priority, tag, useMsg); + + // If this isn't the last node in the chain, move things along. + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java new file mode 100644 index 000000000..19967dcd4 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Simple {@link LogNode} filter, removes everything except the message. + * Useful for situations like on-screen log output where you don't want a lot of metadata displayed, + * just easy-to-read message updates as they're happening. + */ +public class MessageOnlyLogFilter implements LogNode { + + LogNode mNext; + + /** + * Takes the "next" LogNode as a parameter, to simplify chaining. + * + * @param next The next LogNode in the pipeline. + */ + public MessageOnlyLogFilter(LogNode next) { + mNext = next; + } + + public MessageOnlyLogFilter() { + } + + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + if (mNext != null) { + getNext().println(Log.NONE, null, msg, null); + } + } + + /** + * Returns the next LogNode in the chain. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/AssetUtils.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/AssetUtils.java new file mode 100644 index 000000000..b9ad8d5bd --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/AssetUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.recipeassistant; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; + +final class AssetUtils { + private static final String TAG = "RecipeAssistant"; + + public static byte[] loadAsset(Context context, String asset) { + byte[] buffer = null; + try { + InputStream is = context.getAssets().open(asset); + int size = is.available(); + buffer = new byte[size]; + is.read(buffer); + is.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to load asset " + asset + ": " + e); + } + return buffer; + } + + public static JSONObject loadJSONAsset(Context context, String asset) { + String jsonString = new String(loadAsset(context, asset)); + JSONObject jsonObject = null; + try { + jsonObject = new JSONObject(jsonString); + } catch (JSONException e) { + Log.e(TAG, "Failed to parse JSON asset " + asset + ": " + e); + } + return jsonObject; + } + + public static Bitmap loadBitmapAsset(Context context, String asset) { + InputStream is = null; + Bitmap bitmap = null; + try { + is = context.getAssets().open(asset); + if (is != null) { + bitmap = BitmapFactory.decodeStream(is); + } + } catch (IOException e) { + Log.e(TAG, e.toString()); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + Log.e(TAG, "Cannot close InputStream: ", e); + } + } + } + return bitmap; + } +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/Constants.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/Constants.java new file mode 100644 index 000000000..e6d367d77 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/Constants.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.recipeassistant; + +public final class Constants { + private Constants() { + } + public static final String RECIPE_LIST_FILE = "recipelist.json"; + public static final String RECIPE_NAME_TO_LOAD = "recipe_name"; + + public static final String RECIPE_FIELD_LIST = "recipe_list"; + public static final String RECIPE_FIELD_IMAGE = "img"; + public static final String RECIPE_FIELD_INGREDIENTS = "ingredients"; + public static final String RECIPE_FIELD_NAME = "name"; + public static final String RECIPE_FIELD_SUMMARY = "summary"; + public static final String RECIPE_FIELD_STEPS = "steps"; + public static final String RECIPE_FIELD_TEXT = "text"; + public static final String RECIPE_FIELD_TITLE = "title"; + public static final String RECIPE_FIELD_STEP_TEXT = "step_text"; + public static final String RECIPE_FIELD_STEP_IMAGE = "step_image"; + + static final String ACTION_START_COOKING = + "com.example.android.recipeassistant.START_COOKING"; + public static final String EXTRA_RECIPE = "recipe"; + + public static final int NOTIFICATION_ID = 0; + public static final int NOTIFICATION_IMAGE_WIDTH = 280; + public static final int NOTIFICATION_IMAGE_HEIGHT = 280; +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/MainActivity.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/MainActivity.java new file mode 100644 index 000000000..5738e2ad3 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/MainActivity.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.recipeassistant; + +import android.app.ListActivity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.ListView; + +public class MainActivity extends ListActivity { + + private static final String TAG = "RecipeAssistant"; + private RecipeListAdapter mAdapter; + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG , "onListItemClick " + position); + } + String itemName = mAdapter.getItemName(position); + Intent intent = new Intent(getApplicationContext(), RecipeActivity.class); + intent.putExtra(Constants.RECIPE_NAME_TO_LOAD, itemName); + startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(android.R.layout.list_content); + + mAdapter = new RecipeListAdapter(this); + setListAdapter(mAdapter); + } +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/Recipe.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/Recipe.java new file mode 100644 index 000000000..355190735 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/Recipe.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.recipeassistant; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +public class Recipe { + private static final String TAG = "RecipeAssistant"; + + public String titleText; + public String summaryText; + public String recipeImage; + public String ingredientsText; + + public static class RecipeStep { + RecipeStep() { } + public String stepImage; + public String stepText; + + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putString(Constants.RECIPE_FIELD_STEP_TEXT, stepText); + bundle.putString(Constants.RECIPE_FIELD_STEP_IMAGE, stepImage); + return bundle; + } + + public static RecipeStep fromBundle(Bundle bundle) { + RecipeStep recipeStep = new RecipeStep(); + recipeStep.stepText = bundle.getString(Constants.RECIPE_FIELD_STEP_TEXT); + recipeStep.stepImage = bundle.getString(Constants.RECIPE_FIELD_STEP_IMAGE); + return recipeStep; + } + } + ArrayList recipeSteps; + + public Recipe() { + recipeSteps = new ArrayList(); + } + + public static Recipe fromJson(Context context, JSONObject json) { + Recipe recipe = new Recipe(); + try { + recipe.titleText = json.getString(Constants.RECIPE_FIELD_TITLE); + recipe.summaryText = json.getString(Constants.RECIPE_FIELD_SUMMARY); + if (json.has(Constants.RECIPE_FIELD_IMAGE)) { + recipe.recipeImage = json.getString(Constants.RECIPE_FIELD_IMAGE); + } + JSONArray ingredients = json.getJSONArray(Constants.RECIPE_FIELD_INGREDIENTS); + recipe.ingredientsText = ""; + for (int i = 0; i < ingredients.length(); i++) { + recipe.ingredientsText += " - " + + ingredients.getJSONObject(i).getString(Constants.RECIPE_FIELD_TEXT) + "\n"; + } + + JSONArray steps = json.getJSONArray(Constants.RECIPE_FIELD_STEPS); + for (int i = 0; i < steps.length(); i++) { + JSONObject step = steps.getJSONObject(i); + RecipeStep recipeStep = new RecipeStep(); + recipeStep.stepText = step.getString(Constants.RECIPE_FIELD_TEXT); + if (step.has(Constants.RECIPE_FIELD_IMAGE)) { + recipeStep.stepImage = step.getString(Constants.RECIPE_FIELD_IMAGE); + } + recipe.recipeSteps.add(recipeStep); + } + } catch (JSONException e) { + Log.e(TAG, "Error loading recipe: " + e); + return null; + } + return recipe; + } + + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putString(Constants.RECIPE_FIELD_TITLE, titleText); + bundle.putString(Constants.RECIPE_FIELD_SUMMARY, summaryText); + bundle.putString(Constants.RECIPE_FIELD_IMAGE, recipeImage); + bundle.putString(Constants.RECIPE_FIELD_INGREDIENTS, ingredientsText); + if (recipeSteps != null) { + ArrayList stepBundles = new ArrayList(recipeSteps.size()); + for (RecipeStep recipeStep : recipeSteps) { + stepBundles.add(recipeStep.toBundle()); + } + bundle.putParcelableArrayList(Constants.RECIPE_FIELD_STEPS, stepBundles); + } + return bundle; + } + + public static Recipe fromBundle(Bundle bundle) { + Recipe recipe = new Recipe(); + recipe.titleText = bundle.getString(Constants.RECIPE_FIELD_TITLE); + recipe.summaryText = bundle.getString(Constants.RECIPE_FIELD_SUMMARY); + recipe.recipeImage = bundle.getString(Constants.RECIPE_FIELD_IMAGE); + recipe.ingredientsText = bundle.getString(Constants.RECIPE_FIELD_INGREDIENTS); + ArrayList stepBundles = + bundle.getParcelableArrayList(Constants.RECIPE_FIELD_STEPS); + if (stepBundles != null) { + for (Parcelable stepBundle : stepBundles) { + recipe.recipeSteps.add(RecipeStep.fromBundle((Bundle) stepBundle)); + } + } + return recipe; + } +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/RecipeActivity.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/RecipeActivity.java new file mode 100644 index 000000000..4b9d72dd3 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/RecipeActivity.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.recipeassistant; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.json.JSONObject; + +public class RecipeActivity extends Activity { + private static final String TAG = "RecipeAssistant"; + private String mRecipeName; + private Recipe mRecipe; + private ImageView mImageView; + private TextView mTitleTextView; + private TextView mSummaryTextView; + private TextView mIngredientsTextView; + private LinearLayout mStepsLayout; + + @Override + protected void onStart() { + super.onStart(); + Intent intent = getIntent(); + mRecipeName = intent.getStringExtra(Constants.RECIPE_NAME_TO_LOAD); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Intent: " + intent.toString() + " " + mRecipeName); + } + loadRecipe(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.recipe); + mTitleTextView = (TextView) findViewById(R.id.recipeTextTitle); + mSummaryTextView = (TextView) findViewById(R.id.recipeTextSummary); + mImageView = (ImageView) findViewById(R.id.recipeImageView); + mIngredientsTextView = (TextView) findViewById(R.id.textIngredients); + mStepsLayout = (LinearLayout) findViewById(R.id.layoutSteps); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.action_cook: + startCooking(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void loadRecipe() { + JSONObject jsonObject = AssetUtils.loadJSONAsset(this, mRecipeName); + if (jsonObject != null) { + mRecipe = Recipe.fromJson(this, jsonObject); + if (mRecipe != null) { + displayRecipe(mRecipe); + } + } + } + + private void displayRecipe(Recipe recipe) { + Animation fadeIn = AnimationUtils.loadAnimation(this, android.R.anim.fade_in); + mTitleTextView.setAnimation(fadeIn); + mTitleTextView.setText(recipe.titleText); + mSummaryTextView.setText(recipe.summaryText); + if (recipe.recipeImage != null) { + mImageView.setAnimation(fadeIn); + Bitmap recipeImage = AssetUtils.loadBitmapAsset(this, recipe.recipeImage); + mImageView.setImageBitmap(recipeImage); + } + mIngredientsTextView.setText(recipe.ingredientsText); + + findViewById(R.id.ingredientsHeader).setAnimation(fadeIn); + findViewById(R.id.ingredientsHeader).setVisibility(View.VISIBLE); + findViewById(R.id.stepsHeader).setAnimation(fadeIn); + + findViewById(R.id.stepsHeader).setVisibility(View.VISIBLE); + + LayoutInflater inf = LayoutInflater.from(this); + mStepsLayout.removeAllViews(); + int stepNumber = 1; + for (Recipe.RecipeStep step : recipe.recipeSteps) { + View view = inf.inflate(R.layout.step_item, null); + ImageView iv = (ImageView) view.findViewById(R.id.stepImageView); + if (step.stepImage == null) { + iv.setVisibility(View.GONE); + } else { + Bitmap stepImage = AssetUtils.loadBitmapAsset(this, step.stepImage); + iv.setImageBitmap(stepImage); + } + ((TextView) view.findViewById(R.id.textStep)).setText( + (stepNumber++) + ". " + step.stepText); + mStepsLayout.addView(view); + } + } + + private void startCooking() { + Intent intent = new Intent(this, RecipeService.class); + intent.setAction(Constants.ACTION_START_COOKING); + intent.putExtra(Constants.EXTRA_RECIPE, mRecipe.toBundle()); + startService(intent); + } +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/RecipeListAdapter.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/RecipeListAdapter.java new file mode 100644 index 000000000..bc602a181 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/RecipeListAdapter.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.recipeassistant; + +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.Bitmap; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class RecipeListAdapter implements ListAdapter { + private String TAG = "RecipeListAdapter"; + + private class Item { + String title; + String name; + String summary; + Bitmap image; + } + + private List mItems = new ArrayList(); + private Context mContext; + private DataSetObserver mObserver; + + public RecipeListAdapter(Context context) { + mContext = context; + loadRecipeList(); + } + + private void loadRecipeList() { + JSONObject jsonObject = AssetUtils.loadJSONAsset(mContext, Constants.RECIPE_LIST_FILE); + if (jsonObject != null) { + List items = parseJson(jsonObject); + appendItemsToList(items); + } + } + + private List parseJson(JSONObject json) { + List result = new ArrayList(); + try { + JSONArray items = json.getJSONArray(Constants.RECIPE_FIELD_LIST); + for (int i = 0; i < items.length(); i++) { + JSONObject item = items.getJSONObject(i); + Item parsed = new Item(); + parsed.name = item.getString(Constants.RECIPE_FIELD_NAME); + parsed.title = item.getString(Constants.RECIPE_FIELD_TITLE); + if (item.has(Constants.RECIPE_FIELD_IMAGE)) { + String imageFile = item.getString(Constants.RECIPE_FIELD_IMAGE); + parsed.image = AssetUtils.loadBitmapAsset(mContext, imageFile); + } + parsed.summary = item.getString(Constants.RECIPE_FIELD_SUMMARY); + result.add(parsed); + } + } catch (JSONException e) { + Log.e(TAG, "Failed to parse recipe list: " + e); + } + return result; + } + + private void appendItemsToList(List items) { + mItems.addAll(items); + if (mObserver != null) { + mObserver.onChanged(); + } + } + + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mItems.get(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + LayoutInflater inf = LayoutInflater.from(mContext); + view = inf.inflate(R.layout.list_item, null); + } + Item item = (Item) getItem(position); + TextView titleView = (TextView) view.findViewById(R.id.textTitle); + TextView summaryView = (TextView) view.findViewById(R.id.textSummary); + ImageView iv = (ImageView) view.findViewById(R.id.imageView); + + titleView.setText(item.title); + summaryView.setText(item.summary); + if (item.image != null) { + iv.setImageBitmap(item.image); + } else { + iv.setImageDrawable(mContext.getResources().getDrawable(R.drawable.ic_noimage)); + } + return view; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public boolean isEmpty() { + return mItems.isEmpty(); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + mObserver = observer; + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + mObserver = null; + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + public String getItemName(int position) { + return mItems.get(position).name; + } +} diff --git a/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/RecipeService.java b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/RecipeService.java new file mode 100644 index 000000000..74bbfda72 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Application/src/com.example.android.recipeassistant/RecipeService.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.recipeassistant; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Binder; +import android.os.IBinder; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.app.NotificationCompat; + +import java.util.ArrayList; + +public class RecipeService extends Service { + private NotificationManagerCompat mNotificationManager; + private Binder mBinder = new LocalBinder(); + private Recipe mRecipe; + + public class LocalBinder extends Binder { + RecipeService getService() { + return RecipeService.this; + } + } + + @Override + public void onCreate() { + mNotificationManager = NotificationManagerCompat.from(this); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent.getAction().equals(Constants.ACTION_START_COOKING)) { + createNotification(intent); + return START_STICKY; + } + return START_NOT_STICKY; + } + + private void createNotification(Intent intent) { + mRecipe = Recipe.fromBundle(intent.getBundleExtra(Constants.EXTRA_RECIPE)); + ArrayList notificationPages = new ArrayList(); + + int stepCount = mRecipe.recipeSteps.size(); + + for (int i = 0; i < stepCount; ++i) { + Recipe.RecipeStep recipeStep = mRecipe.recipeSteps.get(i); + NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle(); + style.bigText(recipeStep.stepText); + style.setBigContentTitle(String.format( + getResources().getString(R.string.step_count), i + 1, stepCount)); + style.setSummaryText(""); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setStyle(style); + notificationPages.add(builder.build()); + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + + if (mRecipe.recipeImage != null) { + Bitmap recipeImage = Bitmap.createScaledBitmap( + AssetUtils.loadBitmapAsset(this, mRecipe.recipeImage), + Constants.NOTIFICATION_IMAGE_WIDTH, Constants.NOTIFICATION_IMAGE_HEIGHT, false); + builder.setLargeIcon(recipeImage); + } + builder.setContentTitle(mRecipe.titleText); + builder.setContentText(mRecipe.summaryText); + builder.setSmallIcon(R.mipmap.ic_notification_recipe); + + Notification notification = builder + .extend(new NotificationCompat.WearableExtender() + .addPages(notificationPages)) + .build(); + mNotificationManager.notify(Constants.NOTIFICATION_ID, notification); + } +} diff --git a/samples/browseable/RecipeAssistant/Shared/AndroidManifest.xml b/samples/browseable/RecipeAssistant/Shared/AndroidManifest.xml new file mode 100644 index 000000000..1951e2693 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Shared/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/samples/browseable/RecipeAssistant/Shared/res/values/strings.xml b/samples/browseable/RecipeAssistant/Shared/res/values/strings.xml new file mode 100644 index 000000000..0f2bb9075 --- /dev/null +++ b/samples/browseable/RecipeAssistant/Shared/res/values/strings.xml @@ -0,0 +1,18 @@ + + + + Shared + diff --git a/samples/browseable/RecipeAssistant/_index.jd b/samples/browseable/RecipeAssistant/_index.jd new file mode 100644 index 000000000..0860bce87 --- /dev/null +++ b/samples/browseable/RecipeAssistant/_index.jd @@ -0,0 +1,14 @@ +page.tags="RecipeAssistant" +sample.group=Wearable +@jd:body + +

+ + This phone application uses the enhanced notifications API to display recipe + instructions using paged notifications. After starting the application on your phone, you can browse + from a short list of recipes and select one to view. Each recipe is broken down into a number of + steps; when ready, you can click on the START action in the action bar to send the steps to the + wearable. On the wearable device, the steps are displayed as a multi-page notification, with one + page for each step in the recipe. + +

diff --git a/samples/browseable/SkeletonWearableApp/Application/AndroidManifest.xml b/samples/browseable/SkeletonWearableApp/Application/AndroidManifest.xml new file mode 100644 index 000000000..7e1968079 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/samples/browseable/SkeletonWearableApp/Application/res/drawable-hdpi/tile.9.png b/samples/browseable/SkeletonWearableApp/Application/res/drawable-hdpi/tile.9.png new file mode 100644 index 000000000..135862883 Binary files /dev/null and b/samples/browseable/SkeletonWearableApp/Application/res/drawable-hdpi/tile.9.png differ diff --git a/samples/browseable/SkeletonWearableApp/Application/res/layout/activity_main.xml b/samples/browseable/SkeletonWearableApp/Application/res/layout/activity_main.xml new file mode 100755 index 000000000..be1aa49d9 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/res/layout/activity_main.xml @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/samples/browseable/SkeletonWearableApp/Application/res/values-sw600dp/template-dimens.xml b/samples/browseable/SkeletonWearableApp/Application/res/values-sw600dp/template-dimens.xml new file mode 100644 index 000000000..22074a2bd --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/res/values-sw600dp/template-dimens.xml @@ -0,0 +1,24 @@ + + + + + + + @dimen/margin_huge + @dimen/margin_medium + + diff --git a/samples/browseable/SkeletonWearableApp/Application/res/values-sw600dp/template-styles.xml b/samples/browseable/SkeletonWearableApp/Application/res/values-sw600dp/template-styles.xml new file mode 100644 index 000000000..03d197418 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/res/values-sw600dp/template-styles.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/samples/browseable/SkeletonWearableApp/Application/res/values-v11/template-styles.xml b/samples/browseable/SkeletonWearableApp/Application/res/values-v11/template-styles.xml new file mode 100644 index 000000000..8c1ea66f2 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/res/values-v11/template-styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/activities/SampleActivityBase.java b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/activities/SampleActivityBase.java new file mode 100644 index 000000000..3228927b7 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/activities/SampleActivityBase.java @@ -0,0 +1,52 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.example.android.common.activities; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogWrapper; + +/** + * Base launcher activity, to handle most of the common plumbing for samples. + */ +public class SampleActivityBase extends FragmentActivity { + + public static final String TAG = "SampleActivityBase"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void onStart() { + super.onStart(); + initializeLogging(); + } + + /** Set up targets to receive log data */ + public void initializeLogging() { + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + // Wraps Android's native log framework + LogWrapper logWrapper = new LogWrapper(); + Log.setLogNode(logWrapper); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/Log.java b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/Log.java new file mode 100644 index 000000000..17503c568 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/Log.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Helper class for a list (or tree) of LoggerNodes. + * + *

When this is set as the head of the list, + * an instance of it can function as a drop-in replacement for {@link android.util.Log}. + * Most of the methods in this class server only to map a method call in Log to its equivalent + * in LogNode.

+ */ +public class Log { + // Grabbing the native values from Android's native logging facilities, + // to make for easy migration and interop. + public static final int NONE = -1; + public static final int VERBOSE = android.util.Log.VERBOSE; + public static final int DEBUG = android.util.Log.DEBUG; + public static final int INFO = android.util.Log.INFO; + public static final int WARN = android.util.Log.WARN; + public static final int ERROR = android.util.Log.ERROR; + public static final int ASSERT = android.util.Log.ASSERT; + + // Stores the beginning of the LogNode topology. + private static LogNode mLogNode; + + /** + * Returns the next LogNode in the linked list. + */ + public static LogNode getLogNode() { + return mLogNode; + } + + /** + * Sets the LogNode data will be sent to. + */ + public static void setLogNode(LogNode node) { + mLogNode = node; + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void println(int priority, String tag, String msg, Throwable tr) { + if (mLogNode != null) { + mLogNode.println(priority, tag, msg, tr); + } + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + */ + public static void println(int priority, String tag, String msg) { + println(priority, tag, msg, null); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void v(String tag, String msg, Throwable tr) { + println(VERBOSE, tag, msg, tr); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void v(String tag, String msg) { + v(tag, msg, null); + } + + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void d(String tag, String msg, Throwable tr) { + println(DEBUG, tag, msg, tr); + } + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void d(String tag, String msg) { + d(tag, msg, null); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void i(String tag, String msg, Throwable tr) { + println(INFO, tag, msg, tr); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void i(String tag, String msg) { + i(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, String msg, Throwable tr) { + println(WARN, tag, msg, tr); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void w(String tag, String msg) { + w(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, Throwable tr) { + w(tag, null, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void e(String tag, String msg, Throwable tr) { + println(ERROR, tag, msg, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void e(String tag, String msg) { + e(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, String msg, Throwable tr) { + println(ASSERT, tag, msg, tr); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void wtf(String tag, String msg) { + wtf(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, Throwable tr) { + wtf(tag, null, tr); + } +} diff --git a/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogFragment.java b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogFragment.java new file mode 100644 index 000000000..b302acd4b --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogFragment.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.logger; + +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +/** + * Simple fraggment which contains a LogView and uses is to output log data it receives + * through the LogNode interface. + */ +public class LogFragment extends Fragment { + + private LogView mLogView; + private ScrollView mScrollView; + + public LogFragment() {} + + public View inflateViews() { + mScrollView = new ScrollView(getActivity()); + ViewGroup.LayoutParams scrollParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mScrollView.setLayoutParams(scrollParams); + + mLogView = new LogView(getActivity()); + ViewGroup.LayoutParams logParams = new ViewGroup.LayoutParams(scrollParams); + logParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + mLogView.setLayoutParams(logParams); + mLogView.setClickable(true); + mLogView.setFocusable(true); + mLogView.setTypeface(Typeface.MONOSPACE); + + // Want to set padding as 16 dips, setPadding takes pixels. Hooray math! + int paddingDips = 16; + double scale = getResources().getDisplayMetrics().density; + int paddingPixels = (int) ((paddingDips * (scale)) + .5); + mLogView.setPadding(paddingPixels, paddingPixels, paddingPixels, paddingPixels); + mLogView.setCompoundDrawablePadding(paddingPixels); + + mLogView.setGravity(Gravity.BOTTOM); + mLogView.setTextAppearance(getActivity(), android.R.style.TextAppearance_Holo_Medium); + + mScrollView.addView(mLogView); + return mScrollView; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View result = inflateViews(); + + mLogView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + return result; + } + + public LogView getLogView() { + return mLogView; + } +} \ No newline at end of file diff --git a/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogNode.java b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogNode.java new file mode 100644 index 000000000..bc37cabc0 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogNode.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Basic interface for a logging system that can output to one or more targets. + * Note that in addition to classes that will output these logs in some format, + * one can also implement this interface over a filter and insert that in the chain, + * such that no targets further down see certain data, or see manipulated forms of the data. + * You could, for instance, write a "ToHtmlLoggerNode" that just converted all the log data + * it received to HTML and sent it along to the next node in the chain, without printing it + * anywhere. + */ +public interface LogNode { + + /** + * Instructs first LogNode in the list to print the log data provided. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public void println(int priority, String tag, String msg, Throwable tr); + +} diff --git a/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogView.java b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogView.java new file mode 100644 index 000000000..c01542b91 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.app.Activity; +import android.content.Context; +import android.util.*; +import android.widget.TextView; + +/** Simple TextView which is used to output log data received through the LogNode interface. +*/ +public class LogView extends TextView implements LogNode { + + public LogView(Context context) { + super(context); + } + + public LogView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LogView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Formats the log data and prints it out to the LogView. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + + + String priorityStr = null; + + // For the purposes of this View, we want to print the priority as readable text. + switch(priority) { + case android.util.Log.VERBOSE: + priorityStr = "VERBOSE"; + break; + case android.util.Log.DEBUG: + priorityStr = "DEBUG"; + break; + case android.util.Log.INFO: + priorityStr = "INFO"; + break; + case android.util.Log.WARN: + priorityStr = "WARN"; + break; + case android.util.Log.ERROR: + priorityStr = "ERROR"; + break; + case android.util.Log.ASSERT: + priorityStr = "ASSERT"; + break; + default: + break; + } + + // Handily, the Log class has a facility for converting a stack trace into a usable string. + String exceptionStr = null; + if (tr != null) { + exceptionStr = android.util.Log.getStackTraceString(tr); + } + + // Take the priority, tag, message, and exception, and concatenate as necessary + // into one usable line of text. + final StringBuilder outputBuilder = new StringBuilder(); + + String delimiter = "\t"; + appendIfNotNull(outputBuilder, priorityStr, delimiter); + appendIfNotNull(outputBuilder, tag, delimiter); + appendIfNotNull(outputBuilder, msg, delimiter); + appendIfNotNull(outputBuilder, exceptionStr, delimiter); + + // In case this was originally called from an AsyncTask or some other off-UI thread, + // make sure the update occurs within the UI thread. + ((Activity) getContext()).runOnUiThread( (new Thread(new Runnable() { + @Override + public void run() { + // Display the text we just generated within the LogView. + appendToLog(outputBuilder.toString()); + } + }))); + + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } + + public LogNode getNext() { + return mNext; + } + + public void setNext(LogNode node) { + mNext = node; + } + + /** Takes a string and adds to it, with a separator, if the bit to be added isn't null. Since + * the logger takes so many arguments that might be null, this method helps cut out some of the + * agonizing tedium of writing the same 3 lines over and over. + * @param source StringBuilder containing the text to append to. + * @param addStr The String to append + * @param delimiter The String to separate the source and appended strings. A tab or comma, + * for instance. + * @return The fully concatenated String as a StringBuilder + */ + private StringBuilder appendIfNotNull(StringBuilder source, String addStr, String delimiter) { + if (addStr != null) { + if (addStr.length() == 0) { + delimiter = ""; + } + + return source.append(addStr).append(delimiter); + } + return source; + } + + // The next LogNode in the chain. + LogNode mNext; + + /** Outputs the string as a new line of log data in the LogView. */ + public void appendToLog(String s) { + append("\n" + s); + } + + +} diff --git a/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogWrapper.java b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogWrapper.java new file mode 100644 index 000000000..16a9e7ba2 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/LogWrapper.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.util.Log; + +/** + * Helper class which wraps Android's native Log utility in the Logger interface. This way + * normal DDMS output can be one of the many targets receiving and outputting logs simultaneously. + */ +public class LogWrapper implements LogNode { + + // For piping: The next node to receive Log data after this one has done its work. + private LogNode mNext; + + /** + * Returns the next LogNode in the linked list. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + + /** + * Prints data out to the console using Android's native log mechanism. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + // There actually are log methods that don't take a msg parameter. For now, + // if that's the case, just convert null to the empty string and move on. + String useMsg = msg; + if (useMsg == null) { + useMsg = ""; + } + + // If an exeption was provided, convert that exception to a usable string and attach + // it to the end of the msg method. + if (tr != null) { + msg += "\n" + Log.getStackTraceString(tr); + } + + // This is functionally identical to Log.x(tag, useMsg); + // For instance, if priority were Log.VERBOSE, this would be the same as Log.v(tag, useMsg) + Log.println(priority, tag, useMsg); + + // If this isn't the last node in the chain, move things along. + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } +} diff --git a/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java new file mode 100644 index 000000000..19967dcd4 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Application/src/com.example.android.common/logger/MessageOnlyLogFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Simple {@link LogNode} filter, removes everything except the message. + * Useful for situations like on-screen log output where you don't want a lot of metadata displayed, + * just easy-to-read message updates as they're happening. + */ +public class MessageOnlyLogFilter implements LogNode { + + LogNode mNext; + + /** + * Takes the "next" LogNode as a parameter, to simplify chaining. + * + * @param next The next LogNode in the pipeline. + */ + public MessageOnlyLogFilter(LogNode next) { + mNext = next; + } + + public MessageOnlyLogFilter() { + } + + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + if (mNext != null) { + getNext().println(Log.NONE, null, msg, null); + } + } + + /** + * Returns the next LogNode in the chain. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + +} diff --git a/samples/browseable/SkeletonWearableApp/Shared/AndroidManifest.xml b/samples/browseable/SkeletonWearableApp/Shared/AndroidManifest.xml new file mode 100644 index 000000000..5e29d53dc --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Shared/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/samples/browseable/SkeletonWearableApp/Shared/res/values/strings.xml b/samples/browseable/SkeletonWearableApp/Shared/res/values/strings.xml new file mode 100644 index 000000000..0f2bb9075 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Shared/res/values/strings.xml @@ -0,0 +1,18 @@ + + + + Shared + diff --git a/samples/browseable/SkeletonWearableApp/Wearable/AndroidManifest.xml b/samples/browseable/SkeletonWearableApp/Wearable/AndroidManifest.xml new file mode 100644 index 000000000..3e9310e51 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Wearable/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-hdpi/ic_launcher.png b/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-hdpi/ic_launcher.png new file mode 100755 index 000000000..589f229d1 Binary files /dev/null and b/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-mdpi/ic_launcher.png b/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-mdpi/ic_launcher.png new file mode 100755 index 000000000..77dd57139 Binary files /dev/null and b/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-xhdpi/ic_launcher.png b/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-xhdpi/ic_launcher.png new file mode 100755 index 000000000..fe34ebe13 Binary files /dev/null and b/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-xxhdpi/ic_launcher.png b/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-xxhdpi/ic_launcher.png new file mode 100755 index 000000000..ab80bcd13 Binary files /dev/null and b/samples/browseable/SkeletonWearableApp/Wearable/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/samples/browseable/SkeletonWearableApp/Wearable/res/layout/grid_activity.xml b/samples/browseable/SkeletonWearableApp/Wearable/res/layout/grid_activity.xml new file mode 100644 index 000000000..c86705131 --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Wearable/res/layout/grid_activity.xml @@ -0,0 +1,21 @@ + + + + diff --git a/samples/browseable/SkeletonWearableApp/Wearable/res/layout/main_activity.xml b/samples/browseable/SkeletonWearableApp/Wearable/res/layout/main_activity.xml new file mode 100644 index 000000000..c949e5faf --- /dev/null +++ b/samples/browseable/SkeletonWearableApp/Wearable/res/layout/main_activity.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + +