diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..c5155de --- /dev/null +++ b/.classpath @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51887ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target/ +.idea/ +SproutServer.iml \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..306e725 --- /dev/null +++ b/.project @@ -0,0 +1,37 @@ + + + SproutServer + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + + org.eclipse.jem.workbench.JavaEMFNature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/.settings/.jsdtscope b/.settings/.jsdtscope new file mode 100644 index 0000000..f179e11 --- /dev/null +++ b/.settings/.jsdtscope @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..abdea9a --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..443e085 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/.settings/org.eclipse.wst.common.component b/.settings/org.eclipse.wst.common.component new file mode 100644 index 0000000..f38eef4 --- /dev/null +++ b/.settings/org.eclipse.wst.common.component @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml b/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml new file mode 100644 index 0000000..cc81385 --- /dev/null +++ b/.settings/org.eclipse.wst.common.project.facet.core.prefs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.settings/org.eclipse.wst.common.project.facet.core.xml b/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100644 index 0000000..2dcd63f --- /dev/null +++ b/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.container b/.settings/org.eclipse.wst.jsdt.ui.superType.container new file mode 100644 index 0000000..3bd5d0a --- /dev/null +++ b/.settings/org.eclipse.wst.jsdt.ui.superType.container @@ -0,0 +1 @@ +org.eclipse.wst.jsdt.launching.baseBrowserLibrary \ No newline at end of file diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.name b/.settings/org.eclipse.wst.jsdt.ui.superType.name new file mode 100644 index 0000000..05bd71b --- /dev/null +++ b/.settings/org.eclipse.wst.jsdt.ui.superType.name @@ -0,0 +1 @@ +Window \ No newline at end of file diff --git a/.settings/org.eclipse.wst.validation.prefs b/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 0000000..04cad8c --- /dev/null +++ b/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,2 @@ +disabled=06target +eclipse.preferences.version=1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad3e2c1 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +改行 +行末に2つのスペースを入れる。 + +1行目 ←行末に半角スペース2つ +2行目 +deploy test \ No newline at end of file diff --git a/SproutServerMicro.iml b/SproutServerMicro.iml new file mode 100644 index 0000000..6a96207 --- /dev/null +++ b/SproutServerMicro.iml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/cv97r140.jar b/lib/cv97r140.jar new file mode 100644 index 0000000..b742046 --- /dev/null +++ b/lib/cv97r140.jar Binary files differ diff --git a/lib/j3dcore.jar b/lib/j3dcore.jar new file mode 100644 index 0000000..aea76e1 --- /dev/null +++ b/lib/j3dcore.jar Binary files differ diff --git a/lib/j3dutils.jar b/lib/j3dutils.jar new file mode 100644 index 0000000..0e2e9c7 --- /dev/null +++ b/lib/j3dutils.jar Binary files differ diff --git a/lib/vecmath.jar b/lib/vecmath.jar new file mode 100644 index 0000000..6d8b3a1 --- /dev/null +++ b/lib/vecmath.jar Binary files differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fc5e863 --- /dev/null +++ b/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + org.ntlab + SproutServer + war + 0.0.1-SNAPSHOT + SproutServer + + + SproutServer + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + true + + 1.7 + 1.7 + + + + + + + + + org.glassfish.jersey + jersey-bom + ${jersey.version} + pom + import + + + + + + + org.glassfish.jersey.containers + jersey-container-servlet-core + + + + + + net.arnx + jsonic + 1.3.0 + + + + + + j3dcore.jar + j3dcore.jar + system + 1.5.1 + ${basedir}/lib/j3dcore.jar + + + + + cv97r140.jar + cv97r140.jar + system + 1.5.1 + ${basedir}/lib/cv97r140.jar + + + + + j3dutils.jar + j3dutils.jar + system + 1.5.1 + ${basedir}/lib/j3dutils.jar + + + + + vecmath.jar + vecmath.jar + system + 1.5.1 + ${basedir}/lib/vecmath.jar + + + + 2.26-b03 + UTF-8 + + diff --git a/src/main/java/android/os/AidlTest.java b/src/main/java/android/os/AidlTest.java new file mode 100644 index 0000000..bf11d56 --- /dev/null +++ b/src/main/java/android/os/AidlTest.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.os.IInterface; +import android.os.Parcel; +import android.os.Parcelable; +import android.test.suitebuilder.annotation.SmallTest; +import com.google.android.collect.Lists; +import junit.framework.TestCase; + +import java.util.List; + +public class AidlTest extends TestCase { + + private IAidlTest mRemote; + + @Override + protected void setUp() throws Exception { + super.setUp(); + AidlObject mLocal = new AidlObject(); + mRemote = IAidlTest.Stub.asInterface(mLocal); + } + + private static boolean check(TestParcelable p, int n, String s) { + return p.mAnInt == n && + ((s == null && p.mAString == null) || s.equals(p.mAString)); + } + + public static class TestParcelable implements Parcelable { + public int mAnInt; + public String mAString; + + public TestParcelable() { + } + + public TestParcelable(int i, String s) { + mAnInt = i; + mAString = s; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mAnInt); + parcel.writeString(mAString); + } + + public void readFromParcel(Parcel parcel) { + mAnInt = parcel.readInt(); + mAString = parcel.readString(); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public TestParcelable createFromParcel(Parcel parcel) { + return new TestParcelable(parcel.readInt(), + parcel.readString()); + } + + public TestParcelable[] newArray(int size) { + return new TestParcelable[size]; + } + }; + + public String toString() { + return super.toString() + " {" + mAnInt + "/" + mAString + "}"; + } + } + + private static class AidlObject extends IAidlTest.Stub { + public IInterface queryLocalInterface(String descriptor) { + // overriding this to return null makes asInterface always + // generate a proxy + return null; + } + + public int intMethod(int a) { + return a; + } + + public TestParcelable parcelableIn(TestParcelable p) { + p.mAnInt++; + return p; + } + + public TestParcelable parcelableOut(TestParcelable p) { + p.mAnInt = 44; + return p; + } + + public TestParcelable parcelableInOut(TestParcelable p) { + p.mAnInt++; + return p; + } + + public TestParcelable listParcelableLonger(List list, int index) { + list.add(list.get(index)); + return list.get(index); + } + + public int listParcelableShorter(List list, int index) { + list.remove(index); + return list.size(); + } + + public boolean[] booleanArray(boolean[] a0, boolean[] a1, boolean[] a2) { + for (int i = 0; i < a0.length && i < a2.length; i++) { + a2[i] = a0[i]; + } + for (int i = 0; i < a0.length && i < a1.length; i++) { + a1[i] = a0[i]; + } + return a0; + } + + public char[] charArray(char[] a0, char[] a1, char[] a2) { + for (int i = 0; i < a0.length && i < a2.length; i++) { + a2[i] = a0[i]; + } + for (int i = 0; i < a0.length && i < a1.length; i++) { + a1[i] = a0[i]; + } + return a0; + } + + public int[] intArray(int[] a0, int[] a1, int[] a2) { + for (int i = 0; i < a0.length && i < a2.length; i++) { + a2[i] = a0[i]; + } + for (int i = 0; i < a0.length && i < a1.length; i++) { + a1[i] = a0[i]; + } + return a0; + } + + public long[] longArray(long[] a0, long[] a1, long[] a2) { + for (int i = 0; i < a0.length && i < a2.length; i++) { + a2[i] = a0[i]; + } + for (int i = 0; i < a0.length && i < a1.length; i++) { + a1[i] = a0[i]; + } + return a0; + } + + public float[] floatArray(float[] a0, float[] a1, float[] a2) { + for (int i = 0; i < a0.length && i < a2.length; i++) { + a2[i] = a0[i]; + } + for (int i = 0; i < a0.length && i < a1.length; i++) { + a1[i] = a0[i]; + } + return a0; + } + + public double[] doubleArray(double[] a0, double[] a1, double[] a2) { + for (int i = 0; i < a0.length && i < a2.length; i++) { + a2[i] = a0[i]; + } + for (int i = 0; i < a0.length && i < a1.length; i++) { + a1[i] = a0[i]; + } + return a0; + } + + public String[] stringArray(String[] a0, String[] a1, String[] a2) { + for (int i = 0; i < a0.length && i < a2.length; i++) { + a2[i] = a0[i]; + } + for (int i = 0; i < a0.length && i < a1.length; i++) { + a1[i] = a0[i]; + } + return a0; + } + + public TestParcelable[] parcelableArray(TestParcelable[] a0, + TestParcelable[] a1, TestParcelable[] a2) { + return null; + } + + public void voidSecurityException() { + throw new SecurityException("gotcha!"); + } + + public int intSecurityException() { + throw new SecurityException("gotcha!"); + } + } + + @SmallTest + public void testInt() throws Exception { + int result = mRemote.intMethod(42); + assertEquals(42, result); + } + + @SmallTest + public void testParcelableIn() throws Exception { + TestParcelable arg = new TestParcelable(43, "hi"); + TestParcelable result = mRemote.parcelableIn(arg); + assertNotSame(arg, result); + + assertEquals(43, arg.mAnInt); + assertEquals(44, result.mAnInt); + } + + @SmallTest + public void testParcelableOut() throws Exception { + TestParcelable arg = new TestParcelable(43, "hi"); + TestParcelable result = mRemote.parcelableOut(arg); + assertNotSame(arg, result); + assertEquals(44, arg.mAnInt); + } + + @SmallTest + public void testParcelableInOut() throws Exception { + TestParcelable arg = new TestParcelable(43, "hi"); + TestParcelable result = mRemote.parcelableInOut(arg); + assertNotSame(arg, result); + assertEquals(44, arg.mAnInt); + } + + @SmallTest + public void testListParcelableLonger() throws Exception { + List list = Lists.newArrayList(); + list.add(new TestParcelable(33, "asdf")); + list.add(new TestParcelable(34, "jkl;")); + + TestParcelable result = mRemote.listParcelableLonger(list, 1); + +// System.out.println("result=" + result); +// for (TestParcelable p : list) { +// System.out.println("longer: " + p); +// } + + assertEquals("jkl;", result.mAString); + assertEquals(34, result.mAnInt); + + assertEquals(3, list.size()); + assertTrue("out parameter 0: " + list.get(0), check(list.get(0), 33, "asdf")); + assertTrue("out parameter 1: " + list.get(1), check(list.get(1), 34, "jkl;")); + assertTrue("out parameter 2: " + list.get(2), check(list.get(2), 34, "jkl;")); + + assertNotSame(list.get(1), list.get(2)); + } + + @SmallTest + public void testListParcelableShorter() throws Exception { + List list = Lists.newArrayList(); + list.add(new TestParcelable(33, "asdf")); + list.add(new TestParcelable(34, "jkl;")); + list.add(new TestParcelable(35, "qwerty")); + + int result = mRemote.listParcelableShorter(list, 2); + +// System.out.println("result=" + result); +// for (TestParcelable p : list) { +// System.out.println("shorter: " + p); +// } + + assertEquals(2, result); + assertEquals(2, list.size()); + assertTrue("out parameter 0: " + list.get(0), check(list.get(0), 33, "asdf")); + assertTrue("out parameter 1: " + list.get(1), check(list.get(1), 34, "jkl;")); + + assertNotSame(list.get(0), list.get(1)); + } + + @SmallTest + public void testArrays() throws Exception { + // boolean + boolean[] b0 = new boolean[]{true}; + boolean[] b1 = new boolean[]{false, true}; + boolean[] b2 = new boolean[]{true, false, true}; + boolean[] br = mRemote.booleanArray(b0, b1, b2); + + assertEquals(1, br.length); + assertTrue(br[0]); + + assertTrue(b1[0]); + assertFalse(b1[1]); + + assertTrue(b2[0]); + assertFalse(b2[1]); + assertTrue(b2[2]); + + // char + char[] c0 = new char[]{'a'}; + char[] c1 = new char[]{'b', 'c'}; + char[] c2 = new char[]{'d', 'e', 'f'}; + char[] cr = mRemote.charArray(c0, c1, c2); + + assertEquals(1, cr.length); + assertEquals('a', cr[0]); + + assertEquals('a', c1[0]); + assertEquals('\0', c1[1]); + + assertEquals('a', c2[0]); + assertEquals('e', c2[1]); + assertEquals('f', c2[2]); + + // int + int[] i0 = new int[]{34}; + int[] i1 = new int[]{38, 39}; + int[] i2 = new int[]{42, 43, 44}; + int[] ir = mRemote.intArray(i0, i1, i2); + + assertEquals(1, ir.length); + assertEquals(34, ir[0]); + + assertEquals(34, i1[0]); + assertEquals(0, i1[1]); + + assertEquals(34, i2[0]); + assertEquals(43, i2[1]); + assertEquals(44, i2[2]); + + // long + long[] l0 = new long[]{50}; + long[] l1 = new long[]{51, 52}; + long[] l2 = new long[]{53, 54, 55}; + long[] lr = mRemote.longArray(l0, l1, l2); + + assertEquals(1, lr.length); + assertEquals(50, lr[0]); + + assertEquals(50, l1[0]); + assertEquals(0, l1[1]); + + assertEquals(50, l2[0]); + assertEquals(54, l2[1]); + assertEquals(55, l2[2]); + + // float + float[] f0 = new float[]{90.1f}; + float[] f1 = new float[]{90.2f, 90.3f}; + float[] f2 = new float[]{90.4f, 90.5f, 90.6f}; + float[] fr = mRemote.floatArray(f0, f1, f2); + + assertEquals(1, fr.length); + assertEquals(90.1f, fr[0]); + + assertEquals(90.1f, f1[0]); + assertEquals(0f, f1[1], 0.0f); + + assertEquals(90.1f, f2[0]); + assertEquals(90.5f, f2[1]); + assertEquals(90.6f, f2[2]); + + // double + double[] d0 = new double[]{100.1}; + double[] d1 = new double[]{100.2, 100.3}; + double[] d2 = new double[]{100.4, 100.5, 100.6}; + double[] dr = mRemote.doubleArray(d0, d1, d2); + + assertEquals(1, dr.length); + assertEquals(100.1, dr[0]); + + assertEquals(100.1, d1[0]); + assertEquals(0, d1[1], 0.0); + + assertEquals(100.1, d2[0]); + assertEquals(100.5, d2[1]); + assertEquals(100.6, d2[2]); + + // String + String[] s0 = new String[]{"s0[0]"}; + String[] s1 = new String[]{"s1[0]", "s1[1]"}; + String[] s2 = new String[]{"s2[0]", "s2[1]", "s2[2]"}; + String[] sr = mRemote.stringArray(s0, s1, s2); + + assertEquals(1, sr.length); + assertEquals("s0[0]", sr[0]); + + assertEquals("s0[0]", s1[0]); + assertNull(s1[1]); + + assertEquals("s0[0]", s2[0]); + assertEquals("s2[1]", s2[1]); + assertEquals("s2[2]", s2[2]); + } + + @SmallTest + public void testVoidSecurityException() throws Exception { + boolean good = false; + try { + mRemote.voidSecurityException(); + } catch (SecurityException e) { + good = true; + } + assertEquals(good, true); + } + + @SmallTest + public void testIntSecurityException() throws Exception { + boolean good = false; + try { + mRemote.intSecurityException(); + } catch (SecurityException e) { + good = true; + } + assertEquals(good, true); + } +} + diff --git a/src/main/java/android/os/AsyncResult.java b/src/main/java/android/os/AsyncResult.java new file mode 100644 index 0000000..5bad09d --- /dev/null +++ b/src/main/java/android/os/AsyncResult.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.os.Message; + +/** @hide */ +public class AsyncResult +{ + + /*************************** Instance Variables **************************/ + + // Expect either exception or result to be null + public Object userObj; + public Throwable exception; + public Object result; + + /***************************** Class Methods *****************************/ + + /** Saves and sets m.obj */ + public static AsyncResult + forMessage(Message m, Object r, Throwable ex) + { + AsyncResult ret; + + ret = new AsyncResult (m.obj, r, ex); + + m.obj = ret; + + return ret; + } + + /** Saves and sets m.obj */ + public static AsyncResult + forMessage(Message m) + { + AsyncResult ret; + + ret = new AsyncResult (m.obj, null, null); + + m.obj = ret; + + return ret; + } + + /** please note, this sets m.obj to be this */ + public + AsyncResult (Object uo, Object r, Throwable ex) + { + userObj = uo; + result = r; + exception = ex; + } +} diff --git a/src/main/java/android/os/AsyncTask.java b/src/main/java/android/os/AsyncTask.java new file mode 100644 index 0000000..7785f2b --- /dev/null +++ b/src/main/java/android/os/AsyncTask.java @@ -0,0 +1,676 @@ +/* + * Copyright (C) 2008 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 android.os; + +import java.util.ArrayDeque; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + *

AsyncTask enables proper and easy use of the UI thread. This class allows to + * perform background operations and publish results on the UI thread without + * having to manipulate threads and/or handlers.

+ * + *

AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler} + * and does not constitute a generic threading framework. AsyncTasks should ideally be + * used for short operations (a few seconds at the most.) If you need to keep threads + * running for long periods of time, it is highly recommended you use the various APIs + * provided by the java.util.concurrent package such as {@link Executor}, + * {@link ThreadPoolExecutor} and {@link FutureTask}.

+ * + *

An asynchronous task is defined by a computation that runs on a background thread and + * whose result is published on the UI thread. An asynchronous task is defined by 3 generic + * types, called Params, Progress and Result, + * and 4 steps, called onPreExecute, doInBackground, + * onProgressUpdate and onPostExecute.

+ * + *
+ *

Developer Guides

+ *

For more information about using tasks and threads, read the + * Processes and + * Threads developer guide.

+ *
+ * + *

Usage

+ *

AsyncTask must be subclassed to be used. The subclass will override at least + * one method ({@link #doInBackground}), and most often will override a + * second one ({@link #onPostExecute}.)

+ * + *

Here is an example of subclassing:

+ *
+ * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
+ *     protected Long doInBackground(URL... urls) {
+ *         int count = urls.length;
+ *         long totalSize = 0;
+ *         for (int i = 0; i < count; i++) {
+ *             totalSize += Downloader.downloadFile(urls[i]);
+ *             publishProgress((int) ((i / (float) count) * 100));
+ *             // Escape early if cancel() is called
+ *             if (isCancelled()) break;
+ *         }
+ *         return totalSize;
+ *     }
+ *
+ *     protected void onProgressUpdate(Integer... progress) {
+ *         setProgressPercent(progress[0]);
+ *     }
+ *
+ *     protected void onPostExecute(Long result) {
+ *         showDialog("Downloaded " + result + " bytes");
+ *     }
+ * }
+ * 
+ * + *

Once created, a task is executed very simply:

+ *
+ * new DownloadFilesTask().execute(url1, url2, url3);
+ * 
+ * + *

AsyncTask's generic types

+ *

The three types used by an asynchronous task are the following:

+ *
    + *
  1. Params, the type of the parameters sent to the task upon + * execution.
  2. + *
  3. Progress, the type of the progress units published during + * the background computation.
  4. + *
  5. Result, the type of the result of the background + * computation.
  6. + *
+ *

Not all types are always used by an asynchronous task. To mark a type as unused, + * simply use the type {@link Void}:

+ *
+ * private class MyTask extends AsyncTask<Void, Void, Void> { ... }
+ * 
+ * + *

The 4 steps

+ *

When an asynchronous task is executed, the task goes through 4 steps:

+ *
    + *
  1. {@link #onPreExecute()}, invoked on the UI thread before the task + * is executed. This step is normally used to setup the task, for instance by + * showing a progress bar in the user interface.
  2. + *
  3. {@link #doInBackground}, invoked on the background thread + * immediately after {@link #onPreExecute()} finishes executing. This step is used + * to perform background computation that can take a long time. The parameters + * of the asynchronous task are passed to this step. The result of the computation must + * be returned by this step and will be passed back to the last step. This step + * can also use {@link #publishProgress} to publish one or more units + * of progress. These values are published on the UI thread, in the + * {@link #onProgressUpdate} step.
  4. + *
  5. {@link #onProgressUpdate}, invoked on the UI thread after a + * call to {@link #publishProgress}. The timing of the execution is + * undefined. This method is used to display any form of progress in the user + * interface while the background computation is still executing. For instance, + * it can be used to animate a progress bar or show logs in a text field.
  6. + *
  7. {@link #onPostExecute}, invoked on the UI thread after the background + * computation finishes. The result of the background computation is passed to + * this step as a parameter.
  8. + *
+ * + *

Cancelling a task

+ *

A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking + * this method will cause subsequent calls to {@link #isCancelled()} to return true. + * After invoking this method, {@link #onCancelled(Object)}, instead of + * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} + * returns. To ensure that a task is cancelled as quickly as possible, you should always + * check the return value of {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)

+ * + *

Threading rules

+ *

There are a few threading rules that must be followed for this class to + * work properly:

+ *
    + *
  • The AsyncTask class must be loaded on the UI thread. This is done + * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
  • + *
  • The task instance must be created on the UI thread.
  • + *
  • {@link #execute} must be invoked on the UI thread.
  • + *
  • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, + * {@link #doInBackground}, {@link #onProgressUpdate} manually.
  • + *
  • The task can be executed only once (an exception will be thrown if + * a second execution is attempted.)
  • + *
+ * + *

Memory observability

+ *

AsyncTask guarantees that all callback calls are synchronized in such a way that the following + * operations are safe without explicit synchronizations.

+ *
    + *
  • Set member fields in the constructor or {@link #onPreExecute}, and refer to them + * in {@link #doInBackground}. + *
  • Set member fields in {@link #doInBackground}, and refer to them in + * {@link #onProgressUpdate} and {@link #onPostExecute}. + *
+ * + *

Order of execution

+ *

When first introduced, AsyncTasks were executed serially on a single background + * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. Starting with + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single + * thread to avoid common application errors caused by parallel execution.

+ *

If you truly want parallel execution, you can invoke + * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with + * {@link #THREAD_POOL_EXECUTOR}.

+ */ +public abstract class AsyncTask { + private static final String LOG_TAG = "AsyncTask"; + + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final int CORE_POOL_SIZE = CPU_COUNT + 1; + private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; + private static final int KEEP_ALIVE = 1; + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); + } + }; + + private static final BlockingQueue sPoolWorkQueue = + new LinkedBlockingQueue(128); + + /** + * An {@link Executor} that can be used to execute tasks in parallel. + */ + public static final Executor THREAD_POOL_EXECUTOR + = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); + + /** + * An {@link Executor} that executes tasks one at a time in serial + * order. This serialization is global to a particular process. + */ + public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); + + private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; + + private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; + private static InternalHandler sHandler; + + private final WorkerRunnable mWorker; + private final FutureTask mFuture; + + private volatile Status mStatus = Status.PENDING; + + private final AtomicBoolean mCancelled = new AtomicBoolean(); + private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); + + private static class SerialExecutor implements Executor { + final ArrayDeque mTasks = new ArrayDeque(); + Runnable mActive; + + public synchronized void execute(final Runnable r) { + mTasks.offer(new Runnable() { + public void run() { + try { + r.run(); + } finally { + scheduleNext(); + } + } + }); + if (mActive == null) { + scheduleNext(); + } + } + + protected synchronized void scheduleNext() { + if ((mActive = mTasks.poll()) != null) { + THREAD_POOL_EXECUTOR.execute(mActive); + } + } + } + + /** + * Indicates the current status of the task. Each status will be set only once + * during the lifetime of a task. + */ + public enum Status { + /** + * Indicates that the task has not been executed yet. + */ + PENDING, + /** + * Indicates that the task is running. + */ + RUNNING, + /** + * Indicates that {@link AsyncTask#onPostExecute} has finished. + */ + FINISHED, + } + + private static Handler getHandler() { + synchronized (AsyncTask.class) { + if (sHandler == null) { + sHandler = new InternalHandler(); + } + return sHandler; + } + } + + /** @hide */ + public static void setDefaultExecutor(Executor exec) { + sDefaultExecutor = exec; + } + + /** + * Creates a new asynchronous task. This constructor must be invoked on the UI thread. + */ + public AsyncTask() { + mWorker = new WorkerRunnable() { + public Result call() throws Exception { + mTaskInvoked.set(true); + + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + //noinspection unchecked + return postResult(doInBackground(mParams)); + } + }; + + mFuture = new FutureTask(mWorker) { + @Override + protected void done() { + try { + postResultIfNotInvoked(get()); + } catch (InterruptedException e) { + android.util.Log.w(LOG_TAG, e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + postResultIfNotInvoked(null); + } + } + }; + } + + private void postResultIfNotInvoked(Result result) { + final boolean wasTaskInvoked = mTaskInvoked.get(); + if (!wasTaskInvoked) { + postResult(result); + } + } + + private Result postResult(Result result) { + @SuppressWarnings("unchecked") + Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT, + new AsyncTaskResult(this, result)); + message.sendToTarget(); + return result; + } + + /** + * Returns the current status of this task. + * + * @return The current status. + */ + public final Status getStatus() { + return mStatus; + } + + /** + * Override this method to perform a computation on a background thread. The + * specified parameters are the parameters passed to {@link #execute} + * by the caller of this task. + * + * This method can call {@link #publishProgress} to publish updates + * on the UI thread. + * + * @param params The parameters of the task. + * + * @return A result, defined by the subclass of this task. + * + * @see #onPreExecute() + * @see #onPostExecute + * @see #publishProgress + */ + protected abstract Result doInBackground(Params... params); + + /** + * Runs on the UI thread before {@link #doInBackground}. + * + * @see #onPostExecute + * @see #doInBackground + */ + protected void onPreExecute() { + } + + /** + *

Runs on the UI thread after {@link #doInBackground}. The + * specified result is the value returned by {@link #doInBackground}.

+ * + *

This method won't be invoked if the task was cancelled.

+ * + * @param result The result of the operation computed by {@link #doInBackground}. + * + * @see #onPreExecute + * @see #doInBackground + * @see #onCancelled(Object) + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onPostExecute(Result result) { + } + + /** + * Runs on the UI thread after {@link #publishProgress} is invoked. + * The specified values are the values passed to {@link #publishProgress}. + * + * @param values The values indicating progress. + * + * @see #publishProgress + * @see #doInBackground + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onProgressUpdate(Progress... values) { + } + + /** + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + *

The default implementation simply invokes {@link #onCancelled()} and + * ignores the result. If you write your own implementation, do not call + * super.onCancelled(result).

+ * + * @param result The result, if any, computed in + * {@link #doInBackground(Object[])}, can be null + * + * @see #cancel(boolean) + * @see #isCancelled() + */ + @SuppressWarnings({"UnusedParameters"}) + protected void onCancelled(Result result) { + onCancelled(); + } + + /** + *

Applications should preferably override {@link #onCancelled(Object)}. + * This method is invoked by the default implementation of + * {@link #onCancelled(Object)}.

+ * + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + * @see #onCancelled(Object) + * @see #cancel(boolean) + * @see #isCancelled() + */ + protected void onCancelled() { + } + + /** + * Returns true if this task was cancelled before it completed + * normally. If you are calling {@link #cancel(boolean)} on the task, + * the value returned by this method should be checked periodically from + * {@link #doInBackground(Object[])} to end the task as soon as possible. + * + * @return true if task was cancelled before it completed + * + * @see #cancel(boolean) + */ + public final boolean isCancelled() { + return mCancelled.get(); + } + + /** + *

Attempts to cancel execution of this task. This attempt will + * fail if the task has already completed, already been cancelled, + * or could not be cancelled for some other reason. If successful, + * and this task has not started when cancel is called, + * this task should never run. If the task has already started, + * then the mayInterruptIfRunning parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task.

+ * + *

Calling this method will result in {@link #onCancelled(Object)} being + * invoked on the UI thread after {@link #doInBackground(Object[])} + * returns. Calling this method guarantees that {@link #onPostExecute(Object)} + * is never invoked. After invoking this method, you should check the + * value returned by {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])} to finish the task as early as + * possible.

+ * + * @param mayInterruptIfRunning true if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + * + * @return false if the task could not be cancelled, + * typically because it has already completed normally; + * true otherwise + * + * @see #isCancelled() + * @see #onCancelled(Object) + */ + public final boolean cancel(boolean mayInterruptIfRunning) { + mCancelled.set(true); + return mFuture.cancel(mayInterruptIfRunning); + } + + /** + * Waits if necessary for the computation to complete, and then + * retrieves its result. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + */ + public final Result get() throws InterruptedException, ExecutionException { + return mFuture.get(); + } + + /** + * Waits if necessary for at most the given time for the computation + * to complete, and then retrieves its result. + * + * @param timeout Time to wait before cancelling the operation. + * @param unit The time unit for the timeout. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + * @throws TimeoutException If the wait timed out. + */ + public final Result get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + return mFuture.get(timeout, unit); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + *

Note: this function schedules the task on a queue for a single background + * thread or pool of threads depending on the platform version. When first + * introduced, AsyncTasks were executed serially on a single background thread. + * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. Starting + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being + * executed on a single thread to avoid common application errors caused + * by parallel execution. If you truly want parallel execution, you can use + * the {@link #executeOnExecutor} version of this method + * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings + * on its use. + * + *

This method must be invoked on the UI thread. + * + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + * + * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) + * @see #execute(Runnable) + */ + public final AsyncTask execute(Params... params) { + return executeOnExecutor(sDefaultExecutor, params); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + *

This method is typically used with {@link #THREAD_POOL_EXECUTOR} to + * allow multiple tasks to run in parallel on a pool of threads managed by + * AsyncTask, however you can also use your own {@link Executor} for custom + * behavior. + * + *

Warning: Allowing multiple tasks to run in parallel from + * a thread pool is generally not what one wants, because the order + * of their operation is not defined. For example, if these tasks are used + * to modify any state in common (such as writing a file due to a button click), + * there are no guarantees on the order of the modifications. + * Without careful work it is possible in rare cases for the newer version + * of the data to be over-written by an older one, leading to obscure data + * loss and stability issues. Such changes are best + * executed in serial; to guarantee such work is serialized regardless of + * platform version you can use this function with {@link #SERIAL_EXECUTOR}. + * + *

This method must be invoked on the UI thread. + * + * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a + * convenient process-wide thread pool for tasks that are loosely coupled. + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + * + * @see #execute(Object[]) + */ + public final AsyncTask executeOnExecutor(Executor exec, + Params... params) { + if (mStatus != Status.PENDING) { + switch (mStatus) { + case RUNNING: + throw new IllegalStateException("Cannot execute task:" + + " the task is already running."); + case FINISHED: + throw new IllegalStateException("Cannot execute task:" + + " the task has already been executed " + + "(a task can be executed only once)"); + } + } + + mStatus = Status.RUNNING; + + onPreExecute(); + + mWorker.mParams = params; + exec.execute(mFuture); + + return this; + } + + /** + * Convenience version of {@link #execute(Object...)} for use with + * a simple Runnable object. See {@link #execute(Object[])} for more + * information on the order of execution. + * + * @see #execute(Object[]) + * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) + */ + public static void execute(Runnable runnable) { + sDefaultExecutor.execute(runnable); + } + + /** + * This method can be invoked from {@link #doInBackground} to + * publish updates on the UI thread while the background computation is + * still running. Each call to this method will trigger the execution of + * {@link #onProgressUpdate} on the UI thread. + * + * {@link #onProgressUpdate} will not be called if the task has been + * canceled. + * + * @param values The progress values to update the UI with. + * + * @see #onProgressUpdate + * @see #doInBackground + */ + protected final void publishProgress(Progress... values) { + if (!isCancelled()) { + getHandler().obtainMessage(MESSAGE_POST_PROGRESS, + new AsyncTaskResult(this, values)).sendToTarget(); + } + } + + private void finish(Result result) { + if (isCancelled()) { + onCancelled(result); + } else { + onPostExecute(result); + } + mStatus = Status.FINISHED; + } + + private static class InternalHandler extends Handler { + public InternalHandler() { + super(Looper.getMainLooper()); + } + + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) + @Override + public void handleMessage(Message msg) { + AsyncTaskResult result = (AsyncTaskResult) msg.obj; + switch (msg.what) { + case MESSAGE_POST_RESULT: + // There is only one result + result.mTask.finish(result.mData[0]); + break; + case MESSAGE_POST_PROGRESS: + result.mTask.onProgressUpdate(result.mData); + break; + } + } + } + + private static abstract class WorkerRunnable implements Callable { + Params[] mParams; + } + + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class AsyncTaskResult { + final AsyncTask mTask; + final Data[] mData; + + AsyncTaskResult(AsyncTask task, Data... data) { + mTask = task; + mData = data; + } + } +} diff --git a/src/main/java/android/os/BadParcelableException.java b/src/main/java/android/os/BadParcelableException.java new file mode 100644 index 0000000..a1c5bb2 --- /dev/null +++ b/src/main/java/android/os/BadParcelableException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2006 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 android.os; +import android.util.AndroidRuntimeException; + +/** + * The object you are calling has died, because its hosting process + * no longer exists. + */ +public class BadParcelableException extends AndroidRuntimeException { + public BadParcelableException(String msg) { + super(msg); + } + public BadParcelableException(Exception cause) { + super(cause); + } +} diff --git a/src/main/java/android/os/BaseBundle.java b/src/main/java/android/os/BaseBundle.java new file mode 100644 index 0000000..1b02141 --- /dev/null +++ b/src/main/java/android/os/BaseBundle.java @@ -0,0 +1,1363 @@ +/* + * 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 android.os; + +import android.util.ArrayMap; +import android.util.Log; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; + +/** + * A mapping from String values to various types. + */ +public class BaseBundle { + private static final String TAG = "Bundle"; + static final boolean DEBUG = false; + + static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L' + static final Parcel EMPTY_PARCEL; + + static { + EMPTY_PARCEL = Parcel.obtain(); + } + + // Invariant - exactly one of mMap / mParcelledData will be null + // (except inside a call to unparcel) + + ArrayMap mMap = null; + + /* + * If mParcelledData is non-null, then mMap will be null and the + * data are stored as a Parcel containing a Bundle. When the data + * are unparcelled, mParcelledData willbe set to null. + */ + Parcel mParcelledData = null; + + /** + * The ClassLoader used when unparcelling data from mParcelledData. + */ + private ClassLoader mClassLoader; + + /** + * Constructs a new, empty Bundle that uses a specific ClassLoader for + * instantiating Parcelable and Serializable objects. + * + * @param loader An explicit ClassLoader to use when instantiating objects + * inside of the Bundle. + * @param capacity Initial size of the ArrayMap. + */ + BaseBundle(ClassLoader loader, int capacity) { + mMap = capacity > 0 ? + new ArrayMap(capacity) : new ArrayMap(); + mClassLoader = loader == null ? getClass().getClassLoader() : loader; + } + + /** + * Constructs a new, empty Bundle. + */ + BaseBundle() { + this((ClassLoader) null, 0); + } + + /** + * Constructs a Bundle whose data is stored as a Parcel. The data + * will be unparcelled on first contact, using the assigned ClassLoader. + * + * @param parcelledData a Parcel containing a Bundle + */ + BaseBundle(Parcel parcelledData) { + readFromParcelInner(parcelledData); + } + + BaseBundle(Parcel parcelledData, int length) { + readFromParcelInner(parcelledData, length); + } + + /** + * Constructs a new, empty Bundle that uses a specific ClassLoader for + * instantiating Parcelable and Serializable objects. + * + * @param loader An explicit ClassLoader to use when instantiating objects + * inside of the Bundle. + */ + BaseBundle(ClassLoader loader) { + this(loader, 0); + } + + /** + * Constructs a new, empty Bundle sized to hold the given number of + * elements. The Bundle will grow as needed. + * + * @param capacity the initial capacity of the Bundle + */ + BaseBundle(int capacity) { + this((ClassLoader) null, capacity); + } + + /** + * Constructs a Bundle containing a copy of the mappings from the given + * Bundle. + * + * @param b a Bundle to be copied. + */ + BaseBundle(BaseBundle b) { + if (b.mParcelledData != null) { + if (b.mParcelledData == EMPTY_PARCEL) { + mParcelledData = EMPTY_PARCEL; + } else { + mParcelledData = Parcel.obtain(); + mParcelledData.appendFrom(b.mParcelledData, 0, b.mParcelledData.dataSize()); + mParcelledData.setDataPosition(0); + } + } else { + mParcelledData = null; + } + + if (b.mMap != null) { + mMap = new ArrayMap(b.mMap); + } else { + mMap = null; + } + + mClassLoader = b.mClassLoader; + } + + /** + * TODO: optimize this later (getting just the value part of a Bundle + * with a single pair) once Bundle.forPair() above is implemented + * with a special single-value Map implementation/serialization. + * + * Note: value in single-pair Bundle may be null. + * + * @hide + */ + public String getPairValue() { + unparcel(); + int size = mMap.size(); + if (size > 1) { + Log.w(TAG, "getPairValue() used on Bundle with multiple pairs."); + } + if (size == 0) { + return null; + } + Object o = mMap.valueAt(0); + try { + return (String) o; + } catch (ClassCastException e) { + typeWarning("getPairValue()", o, "String", e); + return null; + } + } + + /** + * Changes the ClassLoader this Bundle uses when instantiating objects. + * + * @param loader An explicit ClassLoader to use when instantiating objects + * inside of the Bundle. + */ + void setClassLoader(ClassLoader loader) { + mClassLoader = loader; + } + + /** + * Return the ClassLoader currently associated with this Bundle. + */ + ClassLoader getClassLoader() { + return mClassLoader; + } + + /** + * If the underlying data are stored as a Parcel, unparcel them + * using the currently assigned class loader. + */ + /* package */ synchronized void unparcel() { + if (mParcelledData == null) { + if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + + ": no parcelled data"); + return; + } + + if (mParcelledData == EMPTY_PARCEL) { + if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + + ": empty"); + if (mMap == null) { + mMap = new ArrayMap(1); + } else { + mMap.erase(); + } + mParcelledData = null; + return; + } + + int N = mParcelledData.readInt(); + if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + + ": reading " + N + " maps"); + if (N < 0) { + return; + } + if (mMap == null) { + mMap = new ArrayMap(N); + } else { + mMap.erase(); + mMap.ensureCapacity(N); + } + mParcelledData.readArrayMapInternal(mMap, N, mClassLoader); + mParcelledData.recycle(); + mParcelledData = null; + if (DEBUG) Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this)) + + " final map: " + mMap); + } + + /** + * @hide + */ + public boolean isParcelled() { + return mParcelledData != null; + } + + /** + * Returns the number of mappings contained in this Bundle. + * + * @return the number of mappings as an int. + */ + public int size() { + unparcel(); + return mMap.size(); + } + + /** + * Returns true if the mapping of this Bundle is empty, false otherwise. + */ + public boolean isEmpty() { + unparcel(); + return mMap.isEmpty(); + } + + /** + * Removes all elements from the mapping of this Bundle. + */ + public void clear() { + unparcel(); + mMap.clear(); + } + + /** + * Returns true if the given key is contained in the mapping + * of this Bundle. + * + * @param key a String key + * @return true if the key is part of the mapping, false otherwise + */ + public boolean containsKey(String key) { + unparcel(); + return mMap.containsKey(key); + } + + /** + * Returns the entry with the given key as an object. + * + * @param key a String key + * @return an Object, or null + */ + public Object get(String key) { + unparcel(); + return mMap.get(key); + } + + /** + * Removes any entry with the given key from the mapping of this Bundle. + * + * @param key a String key + */ + public void remove(String key) { + unparcel(); + mMap.remove(key); + } + + /** + * Inserts all mappings from the given PersistableBundle into this BaseBundle. + * + * @param bundle a PersistableBundle + */ + public void putAll(PersistableBundle bundle) { + unparcel(); + bundle.unparcel(); + mMap.putAll(bundle.mMap); + } + + /** + * Inserts all mappings from the given Map into this BaseBundle. + * + * @param map a Map + */ + void putAll(Map map) { + unparcel(); + mMap.putAll(map); + } + + /** + * Returns a Set containing the Strings used as keys in this Bundle. + * + * @return a Set of String keys + */ + public Set keySet() { + unparcel(); + return mMap.keySet(); + } + + /** + * Inserts a Boolean value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Boolean, or null + */ + public void putBoolean(String key, boolean value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a byte value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a byte + */ + void putByte(String key, byte value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a char value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a char, or null + */ + void putChar(String key, char value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a short value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a short + */ + void putShort(String key, short value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an int value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value an int, or null + */ + public void putInt(String key, int value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a long value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a long + */ + public void putLong(String key, long value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a float value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a float + */ + void putFloat(String key, float value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a double value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a double + */ + public void putDouble(String key, double value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a String value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a String, or null + */ + public void putString(String key, String value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a CharSequence value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a CharSequence, or null + */ + void putCharSequence(String key, CharSequence value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an ArrayList value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList object, or null + */ + void putIntegerArrayList(String key, ArrayList value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an ArrayList value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList object, or null + */ + void putStringArrayList(String key, ArrayList value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an ArrayList value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList object, or null + */ + void putCharSequenceArrayList(String key, ArrayList value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a Serializable value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Serializable object, or null + */ + void putSerializable(String key, Serializable value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a boolean array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a boolean array object, or null + */ + public void putBooleanArray(String key, boolean[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a byte array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a byte array object, or null + */ + void putByteArray(String key, byte[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a short array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a short array object, or null + */ + void putShortArray(String key, short[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a char array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a char array object, or null + */ + void putCharArray(String key, char[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an int array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an int array object, or null + */ + public void putIntArray(String key, int[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a long array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a long array object, or null + */ + public void putLongArray(String key, long[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a float array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a float array object, or null + */ + void putFloatArray(String key, float[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a double array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a double array object, or null + */ + public void putDoubleArray(String key, double[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a String array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a String array object, or null + */ + public void putStringArray(String key, String[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a CharSequence array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a CharSequence array object, or null + */ + void putCharSequenceArray(String key, CharSequence[] value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Returns the value associated with the given key, or false if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a boolean value + */ + public boolean getBoolean(String key) { + unparcel(); + if (DEBUG) Log.d(TAG, "Getting boolean in " + + Integer.toHexString(System.identityHashCode(this))); + return getBoolean(key, false); + } + + // Log a message if the value was non-null but not of the expected type + void typeWarning(String key, Object value, String className, + Object defaultValue, ClassCastException e) { + StringBuilder sb = new StringBuilder(); + sb.append("Key "); + sb.append(key); + sb.append(" expected "); + sb.append(className); + sb.append(" but value was a "); + sb.append(value.getClass().getName()); + sb.append(". The default value "); + sb.append(defaultValue); + sb.append(" was returned."); + Log.w(TAG, sb.toString()); + Log.w(TAG, "Attempt to cast generated internal exception:", e); + } + + void typeWarning(String key, Object value, String className, + ClassCastException e) { + typeWarning(key, value, className, "", e); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a boolean value + */ + public boolean getBoolean(String key, boolean defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Boolean) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Boolean", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or (byte) 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a byte value + */ + byte getByte(String key) { + unparcel(); + return getByte(key, (byte) 0); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a byte value + */ + Byte getByte(String key, byte defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Byte) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Byte", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or (char) 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a char value + */ + char getChar(String key) { + unparcel(); + return getChar(key, (char) 0); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a char value + */ + char getChar(String key, char defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Character) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Character", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or (short) 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a short value + */ + short getShort(String key) { + unparcel(); + return getShort(key, (short) 0); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a short value + */ + short getShort(String key, short defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Short) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Short", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return an int value + */ + public int getInt(String key) { + unparcel(); + return getInt(key, 0); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return an int value + */ + public int getInt(String key, int defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Integer) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Integer", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or 0L if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a long value + */ + public long getLong(String key) { + unparcel(); + return getLong(key, 0L); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a long value + */ + public long getLong(String key, long defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Long) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Long", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or 0.0f if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a float value + */ + float getFloat(String key) { + unparcel(); + return getFloat(key, 0.0f); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a float value + */ + float getFloat(String key, float defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Float) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Float", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or 0.0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a double value + */ + public double getDouble(String key) { + unparcel(); + return getDouble(key, 0.0); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a double value + */ + public double getDouble(String key, double defaultValue) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return defaultValue; + } + try { + return (Double) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Double", defaultValue, e); + return defaultValue; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a String value, or null + */ + public String getString(String key) { + unparcel(); + final Object o = mMap.get(key); + try { + return (String) o; + } catch (ClassCastException e) { + typeWarning(key, o, "String", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key or if a null + * value is explicitly associated with the given key. + * + * @param key a String, or null + * @param defaultValue Value to return if key does not exist or if a null + * value is associated with the given key. + * @return the String value associated with the given key, or defaultValue + * if no valid String object is currently mapped to that key. + */ + public String getString(String key, String defaultValue) { + final String s = getString(key); + return (s == null) ? defaultValue : s; + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a CharSequence value, or null + */ + CharSequence getCharSequence(String key) { + unparcel(); + final Object o = mMap.get(key); + try { + return (CharSequence) o; + } catch (ClassCastException e) { + typeWarning(key, o, "CharSequence", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key or if a null + * value is explicitly associated with the given key. + * + * @param key a String, or null + * @param defaultValue Value to return if key does not exist or if a null + * value is associated with the given key. + * @return the CharSequence value associated with the given key, or defaultValue + * if no valid CharSequence object is currently mapped to that key. + */ + CharSequence getCharSequence(String key, CharSequence defaultValue) { + final CharSequence cs = getCharSequence(key); + return (cs == null) ? defaultValue : cs; + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Serializable value, or null + */ + Serializable getSerializable(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (Serializable) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Serializable", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an ArrayList value, or null + */ + ArrayList getIntegerArrayList(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (ArrayList) o; + } catch (ClassCastException e) { + typeWarning(key, o, "ArrayList", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an ArrayList value, or null + */ + ArrayList getStringArrayList(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (ArrayList) o; + } catch (ClassCastException e) { + typeWarning(key, o, "ArrayList", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an ArrayList value, or null + */ + ArrayList getCharSequenceArrayList(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (ArrayList) o; + } catch (ClassCastException e) { + typeWarning(key, o, "ArrayList", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a boolean[] value, or null + */ + public boolean[] getBooleanArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (boolean[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "byte[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a byte[] value, or null + */ + byte[] getByteArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (byte[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "byte[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a short[] value, or null + */ + short[] getShortArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (short[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "short[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a char[] value, or null + */ + char[] getCharArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (char[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "char[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an int[] value, or null + */ + public int[] getIntArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (int[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "int[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a long[] value, or null + */ + public long[] getLongArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (long[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "long[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a float[] value, or null + */ + float[] getFloatArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (float[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "float[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a double[] value, or null + */ + public double[] getDoubleArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (double[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "double[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a String[] value, or null + */ + public String[] getStringArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (String[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "String[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a CharSequence[] value, or null + */ + CharSequence[] getCharSequenceArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (CharSequence[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "CharSequence[]", e); + return null; + } + } + + /** + * Writes the Bundle contents to a Parcel, typically in order for + * it to be passed through an IBinder connection. + * @param parcel The parcel to copy this bundle to. + */ + void writeToParcelInner(Parcel parcel, int flags) { + if (mParcelledData != null) { + if (mParcelledData == EMPTY_PARCEL) { + parcel.writeInt(0); + } else { + int length = mParcelledData.dataSize(); + parcel.writeInt(length); + parcel.writeInt(BUNDLE_MAGIC); + parcel.appendFrom(mParcelledData, 0, length); + } + } else { + // Special case for empty bundles. + if (mMap == null || mMap.size() <= 0) { + parcel.writeInt(0); + return; + } + int lengthPos = parcel.dataPosition(); + parcel.writeInt(-1); // dummy, will hold length + parcel.writeInt(BUNDLE_MAGIC); + + int startPos = parcel.dataPosition(); + parcel.writeArrayMapInternal(mMap); + int endPos = parcel.dataPosition(); + + // Backpatch length + parcel.setDataPosition(lengthPos); + int length = endPos - startPos; + parcel.writeInt(length); + parcel.setDataPosition(endPos); + } + } + + /** + * Reads the Parcel contents into this Bundle, typically in order for + * it to be passed through an IBinder connection. + * @param parcel The parcel to overwrite this bundle from. + */ + void readFromParcelInner(Parcel parcel) { + int length = parcel.readInt(); + if (length < 0) { + throw new RuntimeException("Bad length in parcel: " + length); + } + readFromParcelInner(parcel, length); + } + + private void readFromParcelInner(Parcel parcel, int length) { + if (length == 0) { + // Empty Bundle or end of data. + mParcelledData = EMPTY_PARCEL; + return; + } + int magic = parcel.readInt(); + if (magic != BUNDLE_MAGIC) { + //noinspection ThrowableInstanceNeverThrown + throw new IllegalStateException("Bad magic number for Bundle: 0x" + + Integer.toHexString(magic)); + } + + // Advance within this Parcel + int offset = parcel.dataPosition(); + parcel.setDataPosition(offset + length); + + Parcel p = Parcel.obtain(); + p.setDataPosition(0); + p.appendFrom(parcel, offset, length); + if (DEBUG) Log.d(TAG, "Retrieving " + Integer.toHexString(System.identityHashCode(this)) + + ": " + length + " bundle bytes starting at " + offset); + p.setDataPosition(0); + + mParcelledData = p; + } +} diff --git a/src/main/java/android/os/BatteryManager.java b/src/main/java/android/os/BatteryManager.java new file mode 100644 index 0000000..537e993 --- /dev/null +++ b/src/main/java/android/os/BatteryManager.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2008 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 android.os; + +import android.os.BatteryProperty; +import android.os.IBatteryPropertiesRegistrar; +import android.os.RemoteException; +import android.os.ServiceManager; + +/** + * The BatteryManager class contains strings and constants used for values + * in the {@link android.content.Intent#ACTION_BATTERY_CHANGED} Intent, and + * provides a method for querying battery and charging properties. + */ +public class BatteryManager { + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the current status constant. + */ + public static final String EXTRA_STATUS = "status"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the current health constant. + */ + public static final String EXTRA_HEALTH = "health"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * boolean indicating whether a battery is present. + */ + public static final String EXTRA_PRESENT = "present"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer field containing the current battery level, from 0 to + * {@link #EXTRA_SCALE}. + */ + public static final String EXTRA_LEVEL = "level"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the maximum battery level. + */ + public static final String EXTRA_SCALE = "scale"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the resource ID of a small status bar icon + * indicating the current battery state. + */ + public static final String EXTRA_ICON_SMALL = "icon-small"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer indicating whether the device is plugged in to a power + * source; 0 means it is on battery, other constants are different + * types of power sources. + */ + public static final String EXTRA_PLUGGED = "plugged"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the current battery voltage level. + */ + public static final String EXTRA_VOLTAGE = "voltage"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * integer containing the current battery temperature. + */ + public static final String EXTRA_TEMPERATURE = "temperature"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * String describing the technology of the current battery. + */ + public static final String EXTRA_TECHNOLOGY = "technology"; + + /** + * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}: + * Int value set to nonzero if an unsupported charger is attached + * to the device. + * {@hide} + */ + public static final String EXTRA_INVALID_CHARGER = "invalid_charger"; + + // values for "status" field in the ACTION_BATTERY_CHANGED Intent + public static final int BATTERY_STATUS_UNKNOWN = 1; + public static final int BATTERY_STATUS_CHARGING = 2; + public static final int BATTERY_STATUS_DISCHARGING = 3; + public static final int BATTERY_STATUS_NOT_CHARGING = 4; + public static final int BATTERY_STATUS_FULL = 5; + + // values for "health" field in the ACTION_BATTERY_CHANGED Intent + public static final int BATTERY_HEALTH_UNKNOWN = 1; + public static final int BATTERY_HEALTH_GOOD = 2; + public static final int BATTERY_HEALTH_OVERHEAT = 3; + public static final int BATTERY_HEALTH_DEAD = 4; + public static final int BATTERY_HEALTH_OVER_VOLTAGE = 5; + public static final int BATTERY_HEALTH_UNSPECIFIED_FAILURE = 6; + public static final int BATTERY_HEALTH_COLD = 7; + + // values of the "plugged" field in the ACTION_BATTERY_CHANGED intent. + // These must be powers of 2. + /** Power source is an AC charger. */ + public static final int BATTERY_PLUGGED_AC = 1; + /** Power source is a USB port. */ + public static final int BATTERY_PLUGGED_USB = 2; + /** Power source is wireless. */ + public static final int BATTERY_PLUGGED_WIRELESS = 4; + + /** @hide */ + public static final int BATTERY_PLUGGED_ANY = + BATTERY_PLUGGED_AC | BATTERY_PLUGGED_USB | BATTERY_PLUGGED_WIRELESS; + + /* + * Battery property identifiers. These must match the values in + * frameworks/native/include/batteryservice/BatteryService.h + */ + /** Battery capacity in microampere-hours, as an integer. */ + public static final int BATTERY_PROPERTY_CHARGE_COUNTER = 1; + + /** + * Instantaneous battery current in microamperes, as an integer. Positive + * values indicate net current entering the battery from a charge source, + * negative values indicate net current discharging from the battery. + */ + public static final int BATTERY_PROPERTY_CURRENT_NOW = 2; + + /** + * Average battery current in microamperes, as an integer. Positive + * values indicate net current entering the battery from a charge source, + * negative values indicate net current discharging from the battery. + * The time period over which the average is computed may depend on the + * fuel gauge hardware and its configuration. + */ + public static final int BATTERY_PROPERTY_CURRENT_AVERAGE = 3; + + /** + * Remaining battery capacity as an integer percentage of total capacity + * (with no fractional part). + */ + public static final int BATTERY_PROPERTY_CAPACITY = 4; + + /** + * Battery remaining energy in nanowatt-hours, as a long integer. + */ + public static final int BATTERY_PROPERTY_ENERGY_COUNTER = 5; + + private IBatteryPropertiesRegistrar mBatteryPropertiesRegistrar; + + /** + * Query a battery property from the batteryproperties service. + * + * Returns the requested value, or Long.MIN_VALUE if property not + * supported on this system or on other error. + */ + private long queryProperty(int id) { + long ret; + + if (mBatteryPropertiesRegistrar == null) { + IBinder b = ServiceManager.getService("batteryproperties"); + mBatteryPropertiesRegistrar = + IBatteryPropertiesRegistrar.Stub.asInterface(b); + + if (mBatteryPropertiesRegistrar == null) + return Long.MIN_VALUE; + } + + try { + BatteryProperty prop = new BatteryProperty(); + + if (mBatteryPropertiesRegistrar.getProperty(id, prop) == 0) + ret = prop.getLong(); + else + ret = Long.MIN_VALUE; + } catch (RemoteException e) { + ret = Long.MIN_VALUE; + } + + return ret; + } + + /** + * Return the value of a battery property of integer type. If the + * platform does not provide the property queried, this value will + * be Integer.MIN_VALUE. + * + * @param id identifier of the requested property + * + * @return the property value, or Integer.MIN_VALUE if not supported. + */ + public int getIntProperty(int id) { + return (int)queryProperty(id); + } + + /** + * Return the value of a battery property of long type If the + * platform does not provide the property queried, this value will + * be Long.MIN_VALUE. + * + * @param id identifier of the requested property + * + * @return the property value, or Long.MIN_VALUE if not supported. + */ + public long getLongProperty(int id) { + return queryProperty(id); + } +} diff --git a/src/main/java/android/os/BatteryManagerInternal.java b/src/main/java/android/os/BatteryManagerInternal.java new file mode 100644 index 0000000..f3a95b9 --- /dev/null +++ b/src/main/java/android/os/BatteryManagerInternal.java @@ -0,0 +1,49 @@ +/* + * 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 android.os; + +/** + * Battery manager local system service interface. + * + * @hide Only for use within the system server. + */ +public abstract class BatteryManagerInternal { + /** + * Returns true if the device is plugged into any of the specified plug types. + */ + public abstract boolean isPowered(int plugTypeSet); + + /** + * Returns the current plug type. + */ + public abstract int getPlugType(); + + /** + * Returns battery level as a percentage. + */ + public abstract int getBatteryLevel(); + + /** + * Returns whether we currently consider the battery level to be low. + */ + public abstract boolean getBatteryLevelLow(); + + /** + * Returns a non-zero value if an unsupported charger is attached. + */ + public abstract int getInvalidCharger(); +} diff --git a/src/main/java/android/os/BatteryProperties.java b/src/main/java/android/os/BatteryProperties.java new file mode 100644 index 0000000..8f5cf8b --- /dev/null +++ b/src/main/java/android/os/BatteryProperties.java @@ -0,0 +1,94 @@ +/* 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 android.os; + +/** + * {@hide} + */ +public class BatteryProperties implements Parcelable { + public boolean chargerAcOnline; + public boolean chargerUsbOnline; + public boolean chargerWirelessOnline; + public int batteryStatus; + public int batteryHealth; + public boolean batteryPresent; + public int batteryLevel; + public int batteryVoltage; + public int batteryTemperature; + public String batteryTechnology; + + public BatteryProperties() { + } + + public void set(BatteryProperties other) { + chargerAcOnline = other.chargerAcOnline; + chargerUsbOnline = other.chargerUsbOnline; + chargerWirelessOnline = other.chargerWirelessOnline; + batteryStatus = other.batteryStatus; + batteryHealth = other.batteryHealth; + batteryPresent = other.batteryPresent; + batteryLevel = other.batteryLevel; + batteryVoltage = other.batteryVoltage; + batteryTemperature = other.batteryTemperature; + batteryTechnology = other.batteryTechnology; + } + + /* + * Parcel read/write code must be kept in sync with + * frameworks/native/services/batteryservice/BatteryProperties.cpp + */ + + private BatteryProperties(Parcel p) { + chargerAcOnline = p.readInt() == 1 ? true : false; + chargerUsbOnline = p.readInt() == 1 ? true : false; + chargerWirelessOnline = p.readInt() == 1 ? true : false; + batteryStatus = p.readInt(); + batteryHealth = p.readInt(); + batteryPresent = p.readInt() == 1 ? true : false; + batteryLevel = p.readInt(); + batteryVoltage = p.readInt(); + batteryTemperature = p.readInt(); + batteryTechnology = p.readString(); + } + + public void writeToParcel(Parcel p, int flags) { + p.writeInt(chargerAcOnline ? 1 : 0); + p.writeInt(chargerUsbOnline ? 1 : 0); + p.writeInt(chargerWirelessOnline ? 1 : 0); + p.writeInt(batteryStatus); + p.writeInt(batteryHealth); + p.writeInt(batteryPresent ? 1 : 0); + p.writeInt(batteryLevel); + p.writeInt(batteryVoltage); + p.writeInt(batteryTemperature); + p.writeString(batteryTechnology); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public BatteryProperties createFromParcel(Parcel p) { + return new BatteryProperties(p); + } + + public BatteryProperties[] newArray(int size) { + return new BatteryProperties[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/src/main/java/android/os/BatteryProperty.java b/src/main/java/android/os/BatteryProperty.java new file mode 100644 index 0000000..84119bd --- /dev/null +++ b/src/main/java/android/os/BatteryProperty.java @@ -0,0 +1,77 @@ +/* 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 android.os; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Battery properties that may be queried using + * BatteryManager.getProperty()} + */ + +/** + * @hide + */ +public class BatteryProperty implements Parcelable { + private long mValueLong; + + /** + * @hide + */ + public BatteryProperty() { + mValueLong = Long.MIN_VALUE; + } + + /** + * @hide + */ + public long getLong() { + return mValueLong; + } + + /* + * Parcel read/write code must be kept in sync with + * frameworks/native/services/batteryservice/BatteryProperty.cpp + */ + + private BatteryProperty(Parcel p) { + readFromParcel(p); + } + + public void readFromParcel(Parcel p) { + mValueLong = p.readLong(); + } + + public void writeToParcel(Parcel p, int flags) { + p.writeLong(mValueLong); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public BatteryProperty createFromParcel(Parcel p) { + return new BatteryProperty(p); + } + + public BatteryProperty[] newArray(int size) { + return new BatteryProperty[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/src/main/java/android/os/BatteryStats.java b/src/main/java/android/os/BatteryStats.java new file mode 100644 index 0000000..cd45cfb --- /dev/null +++ b/src/main/java/android/os/BatteryStats.java @@ -0,0 +1,4174 @@ +/* + * Copyright (C) 2008 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 android.os; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Formatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.telephony.SignalStrength; +import android.text.format.DateFormat; +import android.util.Printer; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.TimeUtils; +import android.view.Display; +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; + +/** + * A class providing access to battery usage statistics, including information on + * wakelocks, processes, packages, and services. All times are represented in microseconds + * except where indicated otherwise. + * @hide + */ +public abstract class BatteryStats implements Parcelable { + + private static final boolean LOCAL_LOGV = false; + + /** @hide */ + public static final String SERVICE_NAME = "batterystats"; + + /** + * A constant indicating a partial wake lock timer. + */ + public static final int WAKE_TYPE_PARTIAL = 0; + + /** + * A constant indicating a full wake lock timer. + */ + public static final int WAKE_TYPE_FULL = 1; + + /** + * A constant indicating a window wake lock timer. + */ + public static final int WAKE_TYPE_WINDOW = 2; + + /** + * A constant indicating a sensor timer. + */ + public static final int SENSOR = 3; + + /** + * A constant indicating a a wifi running timer + */ + public static final int WIFI_RUNNING = 4; + + /** + * A constant indicating a full wifi lock timer + */ + public static final int FULL_WIFI_LOCK = 5; + + /** + * A constant indicating a wifi scan + */ + public static final int WIFI_SCAN = 6; + + /** + * A constant indicating a wifi multicast timer + */ + public static final int WIFI_MULTICAST_ENABLED = 7; + + /** + * A constant indicating a video turn on timer + */ + public static final int VIDEO_TURNED_ON = 8; + + /** + * A constant indicating a vibrator on timer + */ + public static final int VIBRATOR_ON = 9; + + /** + * A constant indicating a foreground activity timer + */ + public static final int FOREGROUND_ACTIVITY = 10; + + /** + * A constant indicating a wifi batched scan is active + */ + public static final int WIFI_BATCHED_SCAN = 11; + + /** + * A constant indicating a process state timer + */ + public static final int PROCESS_STATE = 12; + + /** + * A constant indicating a sync timer + */ + public static final int SYNC = 13; + + /** + * A constant indicating a job timer + */ + public static final int JOB = 14; + + /** + * A constant indicating an audio turn on timer + */ + public static final int AUDIO_TURNED_ON = 15; + + /** + * Include all of the data in the stats, including previously saved data. + */ + public static final int STATS_SINCE_CHARGED = 0; + + /** + * Include only the current run in the stats. + */ + public static final int STATS_CURRENT = 1; + + /** + * Include only the run since the last time the device was unplugged in the stats. + */ + public static final int STATS_SINCE_UNPLUGGED = 2; + + // NOTE: Update this list if you add/change any stats above. + // These characters are supposed to represent "total", "last", "current", + // and "unplugged". They were shortened for efficiency sake. + private static final String[] STAT_NAMES = { "l", "c", "u" }; + + /** + * Bump the version on this if the checkin format changes. + */ + private static final int BATTERY_STATS_CHECKIN_VERSION = 9; + + private static final long BYTES_PER_KB = 1024; + private static final long BYTES_PER_MB = 1048576; // 1024^2 + private static final long BYTES_PER_GB = 1073741824; //1024^3 + + private static final String VERSION_DATA = "vers"; + private static final String UID_DATA = "uid"; + private static final String APK_DATA = "apk"; + private static final String PROCESS_DATA = "pr"; + private static final String SENSOR_DATA = "sr"; + private static final String VIBRATOR_DATA = "vib"; + private static final String FOREGROUND_DATA = "fg"; + private static final String STATE_TIME_DATA = "st"; + private static final String WAKELOCK_DATA = "wl"; + private static final String SYNC_DATA = "sy"; + private static final String JOB_DATA = "jb"; + private static final String KERNEL_WAKELOCK_DATA = "kwl"; + private static final String WAKEUP_REASON_DATA = "wr"; + private static final String NETWORK_DATA = "nt"; + private static final String USER_ACTIVITY_DATA = "ua"; + private static final String BATTERY_DATA = "bt"; + private static final String BATTERY_DISCHARGE_DATA = "dc"; + private static final String BATTERY_LEVEL_DATA = "lv"; + private static final String WIFI_DATA = "wfl"; + private static final String MISC_DATA = "m"; + private static final String GLOBAL_NETWORK_DATA = "gn"; + private static final String HISTORY_STRING_POOL = "hsp"; + private static final String HISTORY_DATA = "h"; + private static final String SCREEN_BRIGHTNESS_DATA = "br"; + private static final String SIGNAL_STRENGTH_TIME_DATA = "sgt"; + private static final String SIGNAL_SCANNING_TIME_DATA = "sst"; + private static final String SIGNAL_STRENGTH_COUNT_DATA = "sgc"; + private static final String DATA_CONNECTION_TIME_DATA = "dct"; + private static final String DATA_CONNECTION_COUNT_DATA = "dcc"; + private static final String WIFI_STATE_TIME_DATA = "wst"; + private static final String WIFI_STATE_COUNT_DATA = "wsc"; + private static final String WIFI_SUPPL_STATE_TIME_DATA = "wsst"; + private static final String WIFI_SUPPL_STATE_COUNT_DATA = "wssc"; + private static final String WIFI_SIGNAL_STRENGTH_TIME_DATA = "wsgt"; + private static final String WIFI_SIGNAL_STRENGTH_COUNT_DATA = "wsgc"; + private static final String BLUETOOTH_STATE_TIME_DATA = "bst"; + private static final String BLUETOOTH_STATE_COUNT_DATA = "bsc"; + private static final String POWER_USE_SUMMARY_DATA = "pws"; + private static final String POWER_USE_ITEM_DATA = "pwi"; + private static final String DISCHARGE_STEP_DATA = "dsd"; + private static final String CHARGE_STEP_DATA = "csd"; + private static final String DISCHARGE_TIME_REMAIN_DATA = "dtr"; + private static final String CHARGE_TIME_REMAIN_DATA = "ctr"; + + private final StringBuilder mFormatBuilder = new StringBuilder(32); + private final Formatter mFormatter = new Formatter(mFormatBuilder); + + /** + * State for keeping track of counting information. + */ + public static abstract class Counter { + + /** + * Returns the count associated with this Counter for the + * selected type of statistics. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT + */ + public abstract int getCountLocked(int which); + + /** + * Temporary for debugging. + */ + public abstract void logState(Printer pw, String prefix); + } + + /** + * State for keeping track of long counting information. + */ + public static abstract class LongCounter { + + /** + * Returns the count associated with this Counter for the + * selected type of statistics. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT + */ + public abstract long getCountLocked(int which); + + /** + * Temporary for debugging. + */ + public abstract void logState(Printer pw, String prefix); + } + + /** + * State for keeping track of timing information. + */ + public static abstract class Timer { + + /** + * Returns the count associated with this Timer for the + * selected type of statistics. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT + */ + public abstract int getCountLocked(int which); + + /** + * Returns the total time in microseconds associated with this Timer for the + * selected type of statistics. + * + * @param elapsedRealtimeUs current elapsed realtime of system in microseconds + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT + * @return a time in microseconds + */ + public abstract long getTotalTimeLocked(long elapsedRealtimeUs, int which); + + /** + * Temporary for debugging. + */ + public abstract void logState(Printer pw, String prefix); + } + + /** + * The statistics associated with a particular uid. + */ + public static abstract class Uid { + + /** + * Returns a mapping containing wakelock statistics. + * + * @return a Map from Strings to Uid.Wakelock objects. + */ + public abstract Map getWakelockStats(); + + /** + * Returns a mapping containing sync statistics. + * + * @return a Map from Strings to Timer objects. + */ + public abstract Map getSyncStats(); + + /** + * Returns a mapping containing scheduled job statistics. + * + * @return a Map from Strings to Timer objects. + */ + public abstract Map getJobStats(); + + /** + * The statistics associated with a particular wake lock. + */ + public static abstract class Wakelock { + public abstract Timer getWakeTime(int type); + } + + /** + * Returns a mapping containing sensor statistics. + * + * @return a Map from Integer sensor ids to Uid.Sensor objects. + */ + public abstract SparseArray getSensorStats(); + + /** + * Returns a mapping containing active process data. + */ + public abstract SparseArray getPidStats(); + + /** + * Returns a mapping containing process statistics. + * + * @return a Map from Strings to Uid.Proc objects. + */ + public abstract Map getProcessStats(); + + /** + * Returns a mapping containing package statistics. + * + * @return a Map from Strings to Uid.Pkg objects. + */ + public abstract Map getPackageStats(); + + /** + * {@hide} + */ + public abstract int getUid(); + + public abstract void noteWifiRunningLocked(long elapsedRealtime); + public abstract void noteWifiStoppedLocked(long elapsedRealtime); + public abstract void noteFullWifiLockAcquiredLocked(long elapsedRealtime); + public abstract void noteFullWifiLockReleasedLocked(long elapsedRealtime); + public abstract void noteWifiScanStartedLocked(long elapsedRealtime); + public abstract void noteWifiScanStoppedLocked(long elapsedRealtime); + public abstract void noteWifiBatchedScanStartedLocked(int csph, long elapsedRealtime); + public abstract void noteWifiBatchedScanStoppedLocked(long elapsedRealtime); + public abstract void noteWifiMulticastEnabledLocked(long elapsedRealtime); + public abstract void noteWifiMulticastDisabledLocked(long elapsedRealtime); + public abstract void noteActivityResumedLocked(long elapsedRealtime); + public abstract void noteActivityPausedLocked(long elapsedRealtime); + public abstract long getWifiRunningTime(long elapsedRealtimeUs, int which); + public abstract long getFullWifiLockTime(long elapsedRealtimeUs, int which); + public abstract long getWifiScanTime(long elapsedRealtimeUs, int which); + public abstract long getWifiBatchedScanTime(int csphBin, long elapsedRealtimeUs, int which); + public abstract long getWifiMulticastTime(long elapsedRealtimeUs, int which); + public abstract long getAudioTurnedOnTime(long elapsedRealtimeUs, int which); + public abstract long getVideoTurnedOnTime(long elapsedRealtimeUs, int which); + public abstract Timer getForegroundActivityTimer(); + + // Time this uid has any processes in foreground state. + public static final int PROCESS_STATE_FOREGROUND = 0; + // Time this uid has any process in active state (not cached). + public static final int PROCESS_STATE_ACTIVE = 1; + // Time this uid has any processes running at all. + public static final int PROCESS_STATE_RUNNING = 2; + // Total number of process states we track. + public static final int NUM_PROCESS_STATE = 3; + + static final String[] PROCESS_STATE_NAMES = { + "Foreground", "Active", "Running" + }; + + public abstract long getProcessStateTime(int state, long elapsedRealtimeUs, int which); + + public abstract Timer getVibratorOnTimer(); + + public static final int NUM_WIFI_BATCHED_SCAN_BINS = 5; + + /** + * Note that these must match the constants in android.os.PowerManager. + * Also, if the user activity types change, the BatteryStatsImpl.VERSION must + * also be bumped. + */ + static final String[] USER_ACTIVITY_TYPES = { + "other", "button", "touch" + }; + + public static final int NUM_USER_ACTIVITY_TYPES = 3; + + public abstract void noteUserActivityLocked(int type); + public abstract boolean hasUserActivity(); + public abstract int getUserActivityCount(int type, int which); + + public abstract boolean hasNetworkActivity(); + public abstract long getNetworkActivityBytes(int type, int which); + public abstract long getNetworkActivityPackets(int type, int which); + public abstract long getMobileRadioActiveTime(int which); + public abstract int getMobileRadioActiveCount(int which); + + public static abstract class Sensor { + /* + * FIXME: it's not correct to use this magic value because it + * could clash with a sensor handle (which are defined by + * the sensor HAL, and therefore out of our control + */ + // Magic sensor number for the GPS. + public static final int GPS = -10000; + + public abstract int getHandle(); + + public abstract Timer getSensorTime(); + } + + public class Pid { + public int mWakeNesting; + public long mWakeSumMs; + public long mWakeStartMs; + } + + /** + * The statistics associated with a particular process. + */ + public static abstract class Proc { + + public static class ExcessivePower { + public static final int TYPE_WAKE = 1; + public static final int TYPE_CPU = 2; + + public int type; + public long overTime; + public long usedTime; + } + + /** + * Returns true if this process is still active in the battery stats. + */ + public abstract boolean isActive(); + + /** + * Returns the total time (in 1/100 sec) spent executing in user code. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract long getUserTime(int which); + + /** + * Returns the total time (in 1/100 sec) spent executing in system code. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract long getSystemTime(int which); + + /** + * Returns the number of times the process has been started. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract int getStarts(int which); + + /** + * Returns the number of times the process has crashed. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract int getNumCrashes(int which); + + /** + * Returns the number of times the process has ANRed. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract int getNumAnrs(int which); + + /** + * Returns the cpu time spent in microseconds while the process was in the foreground. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + * @return foreground cpu time in microseconds + */ + public abstract long getForegroundTime(int which); + + /** + * Returns the approximate cpu time spent in microseconds, at a certain CPU speed. + * @param speedStep the index of the CPU speed. This is not the actual speed of the + * CPU. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + * @see BatteryStats#getCpuSpeedSteps() + */ + public abstract long getTimeAtCpuSpeedStep(int speedStep, int which); + + public abstract int countExcessivePowers(); + + public abstract ExcessivePower getExcessivePower(int i); + } + + /** + * The statistics associated with a particular package. + */ + public static abstract class Pkg { + + /** + * Returns the number of times this package has done something that could wake up the + * device from sleep. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract int getWakeups(int which); + + /** + * Returns a mapping containing service statistics. + */ + public abstract Map getServiceStats(); + + /** + * The statistics associated with a particular service. + */ + public abstract class Serv { + + /** + * Returns the amount of time spent started. + * + * @param batteryUptime elapsed uptime on battery in microseconds. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + * @return + */ + public abstract long getStartTime(long batteryUptime, int which); + + /** + * Returns the total number of times startService() has been called. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract int getStarts(int which); + + /** + * Returns the total number times the service has been launched. + * + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract int getLaunches(int which); + } + } + } + + public final static class HistoryTag { + public String string; + public int uid; + + public int poolIdx; + + public void setTo(HistoryTag o) { + string = o.string; + uid = o.uid; + poolIdx = o.poolIdx; + } + + public void setTo(String _string, int _uid) { + string = _string; + uid = _uid; + poolIdx = -1; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(string); + dest.writeInt(uid); + } + + public void readFromParcel(Parcel src) { + string = src.readString(); + uid = src.readInt(); + poolIdx = -1; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HistoryTag that = (HistoryTag) o; + + if (uid != that.uid) return false; + if (!string.equals(that.string)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = string.hashCode(); + result = 31 * result + uid; + return result; + } + } + + public final static class HistoryItem implements Parcelable { + public HistoryItem next; + + // The time of this event in milliseconds, as per SystemClock.elapsedRealtime(). + public long time; + + public static final byte CMD_UPDATE = 0; // These can be written as deltas + public static final byte CMD_NULL = -1; + public static final byte CMD_START = 4; + public static final byte CMD_CURRENT_TIME = 5; + public static final byte CMD_OVERFLOW = 6; + public static final byte CMD_RESET = 7; + public static final byte CMD_SHUTDOWN = 8; + + public byte cmd = CMD_NULL; + + /** + * Return whether the command code is a delta data update. + */ + public boolean isDeltaData() { + return cmd == CMD_UPDATE; + } + + public byte batteryLevel; + public byte batteryStatus; + public byte batteryHealth; + public byte batteryPlugType; + + public short batteryTemperature; + public char batteryVoltage; + + // Constants from SCREEN_BRIGHTNESS_* + public static final int STATE_BRIGHTNESS_SHIFT = 0; + public static final int STATE_BRIGHTNESS_MASK = 0x7; + // Constants from SIGNAL_STRENGTH_* + public static final int STATE_PHONE_SIGNAL_STRENGTH_SHIFT = 3; + public static final int STATE_PHONE_SIGNAL_STRENGTH_MASK = 0x7 << STATE_PHONE_SIGNAL_STRENGTH_SHIFT; + // Constants from ServiceState.STATE_* + public static final int STATE_PHONE_STATE_SHIFT = 6; + public static final int STATE_PHONE_STATE_MASK = 0x7 << STATE_PHONE_STATE_SHIFT; + // Constants from DATA_CONNECTION_* + public static final int STATE_DATA_CONNECTION_SHIFT = 9; + public static final int STATE_DATA_CONNECTION_MASK = 0x1f << STATE_DATA_CONNECTION_SHIFT; + + // These states always appear directly in the first int token + // of a delta change; they should be ones that change relatively + // frequently. + public static final int STATE_CPU_RUNNING_FLAG = 1<<31; + public static final int STATE_WAKE_LOCK_FLAG = 1<<30; + public static final int STATE_GPS_ON_FLAG = 1<<29; + public static final int STATE_WIFI_FULL_LOCK_FLAG = 1<<28; + public static final int STATE_WIFI_SCAN_FLAG = 1<<27; + public static final int STATE_WIFI_MULTICAST_ON_FLAG = 1<<26; + public static final int STATE_MOBILE_RADIO_ACTIVE_FLAG = 1<<25; + // These are on the lower bits used for the command; if they change + // we need to write another int of data. + public static final int STATE_SENSOR_ON_FLAG = 1<<23; + public static final int STATE_AUDIO_ON_FLAG = 1<<22; + public static final int STATE_PHONE_SCANNING_FLAG = 1<<21; + public static final int STATE_SCREEN_ON_FLAG = 1<<20; + public static final int STATE_BATTERY_PLUGGED_FLAG = 1<<19; + public static final int STATE_PHONE_IN_CALL_FLAG = 1<<18; + public static final int STATE_BLUETOOTH_ON_FLAG = 1<<16; + + public static final int MOST_INTERESTING_STATES = + STATE_BATTERY_PLUGGED_FLAG | STATE_SCREEN_ON_FLAG + | STATE_PHONE_IN_CALL_FLAG | STATE_BLUETOOTH_ON_FLAG; + + public int states; + + // Constants from WIFI_SUPPL_STATE_* + public static final int STATE2_WIFI_SUPPL_STATE_SHIFT = 0; + public static final int STATE2_WIFI_SUPPL_STATE_MASK = 0xf; + // Values for NUM_WIFI_SIGNAL_STRENGTH_BINS + public static final int STATE2_WIFI_SIGNAL_STRENGTH_SHIFT = 4; + public static final int STATE2_WIFI_SIGNAL_STRENGTH_MASK = + 0x7 << STATE2_WIFI_SIGNAL_STRENGTH_SHIFT; + + public static final int STATE2_LOW_POWER_FLAG = 1<<31; + public static final int STATE2_VIDEO_ON_FLAG = 1<<30; + public static final int STATE2_WIFI_RUNNING_FLAG = 1<<29; + public static final int STATE2_WIFI_ON_FLAG = 1<<28; + public static final int STATE2_FLASHLIGHT_FLAG = 1<<27; + + public static final int MOST_INTERESTING_STATES2 = + STATE2_LOW_POWER_FLAG | STATE2_WIFI_ON_FLAG; + + public int states2; + + // The wake lock that was acquired at this point. + public HistoryTag wakelockTag; + + // Kernel wakeup reason at this point. + public HistoryTag wakeReasonTag; + + public static final int EVENT_FLAG_START = 0x8000; + public static final int EVENT_FLAG_FINISH = 0x4000; + + // No event in this item. + public static final int EVENT_NONE = 0x0000; + // Event is about a process that is running. + public static final int EVENT_PROC = 0x0001; + // Event is about an application package that is in the foreground. + public static final int EVENT_FOREGROUND = 0x0002; + // Event is about an application package that is at the top of the screen. + public static final int EVENT_TOP = 0x0003; + // Event is about active sync operations. + public static final int EVENT_SYNC = 0x0004; + // Events for all additional wake locks aquired/release within a wake block. + // These are not generated by default. + public static final int EVENT_WAKE_LOCK = 0x0005; + // Event is about an application executing a scheduled job. + public static final int EVENT_JOB = 0x0006; + // Events for users running. + public static final int EVENT_USER_RUNNING = 0x0007; + // Events for foreground user. + public static final int EVENT_USER_FOREGROUND = 0x0008; + // Events for connectivity changed. + public static final int EVENT_CONNECTIVITY_CHANGED = 0x0009; + // Number of event types. + public static final int EVENT_COUNT = 0x000a; + // Mask to extract out only the type part of the event. + public static final int EVENT_TYPE_MASK = ~(EVENT_FLAG_START|EVENT_FLAG_FINISH); + + public static final int EVENT_PROC_START = EVENT_PROC | EVENT_FLAG_START; + public static final int EVENT_PROC_FINISH = EVENT_PROC | EVENT_FLAG_FINISH; + public static final int EVENT_FOREGROUND_START = EVENT_FOREGROUND | EVENT_FLAG_START; + public static final int EVENT_FOREGROUND_FINISH = EVENT_FOREGROUND | EVENT_FLAG_FINISH; + public static final int EVENT_TOP_START = EVENT_TOP | EVENT_FLAG_START; + public static final int EVENT_TOP_FINISH = EVENT_TOP | EVENT_FLAG_FINISH; + public static final int EVENT_SYNC_START = EVENT_SYNC | EVENT_FLAG_START; + public static final int EVENT_SYNC_FINISH = EVENT_SYNC | EVENT_FLAG_FINISH; + public static final int EVENT_WAKE_LOCK_START = EVENT_WAKE_LOCK | EVENT_FLAG_START; + public static final int EVENT_WAKE_LOCK_FINISH = EVENT_WAKE_LOCK | EVENT_FLAG_FINISH; + public static final int EVENT_JOB_START = EVENT_JOB | EVENT_FLAG_START; + public static final int EVENT_JOB_FINISH = EVENT_JOB | EVENT_FLAG_FINISH; + public static final int EVENT_USER_RUNNING_START = EVENT_USER_RUNNING | EVENT_FLAG_START; + public static final int EVENT_USER_RUNNING_FINISH = EVENT_USER_RUNNING | EVENT_FLAG_FINISH; + public static final int EVENT_USER_FOREGROUND_START = + EVENT_USER_FOREGROUND | EVENT_FLAG_START; + public static final int EVENT_USER_FOREGROUND_FINISH = + EVENT_USER_FOREGROUND | EVENT_FLAG_FINISH; + + // For CMD_EVENT. + public int eventCode; + public HistoryTag eventTag; + + // Only set for CMD_CURRENT_TIME or CMD_RESET, as per System.currentTimeMillis(). + public long currentTime; + + // Meta-data when reading. + public int numReadInts; + + // Pre-allocated objects. + public final HistoryTag localWakelockTag = new HistoryTag(); + public final HistoryTag localWakeReasonTag = new HistoryTag(); + public final HistoryTag localEventTag = new HistoryTag(); + + public HistoryItem() { + } + + public HistoryItem(long time, Parcel src) { + this.time = time; + numReadInts = 2; + readFromParcel(src); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(time); + int bat = (((int)cmd)&0xff) + | ((((int)batteryLevel)<<8)&0xff00) + | ((((int)batteryStatus)<<16)&0xf0000) + | ((((int)batteryHealth)<<20)&0xf00000) + | ((((int)batteryPlugType)<<24)&0xf000000) + | (wakelockTag != null ? 0x10000000 : 0) + | (wakeReasonTag != null ? 0x20000000 : 0) + | (eventCode != EVENT_NONE ? 0x40000000 : 0); + dest.writeInt(bat); + bat = (((int)batteryTemperature)&0xffff) + | ((((int)batteryVoltage)<<16)&0xffff0000); + dest.writeInt(bat); + dest.writeInt(states); + dest.writeInt(states2); + if (wakelockTag != null) { + wakelockTag.writeToParcel(dest, flags); + } + if (wakeReasonTag != null) { + wakeReasonTag.writeToParcel(dest, flags); + } + if (eventCode != EVENT_NONE) { + dest.writeInt(eventCode); + eventTag.writeToParcel(dest, flags); + } + if (cmd == CMD_CURRENT_TIME || cmd == CMD_RESET) { + dest.writeLong(currentTime); + } + } + + public void readFromParcel(Parcel src) { + int start = src.dataPosition(); + int bat = src.readInt(); + cmd = (byte)(bat&0xff); + batteryLevel = (byte)((bat>>8)&0xff); + batteryStatus = (byte)((bat>>16)&0xf); + batteryHealth = (byte)((bat>>20)&0xf); + batteryPlugType = (byte)((bat>>24)&0xf); + int bat2 = src.readInt(); + batteryTemperature = (short)(bat2&0xffff); + batteryVoltage = (char)((bat2>>16)&0xffff); + states = src.readInt(); + states2 = src.readInt(); + if ((bat&0x10000000) != 0) { + wakelockTag = localWakelockTag; + wakelockTag.readFromParcel(src); + } else { + wakelockTag = null; + } + if ((bat&0x20000000) != 0) { + wakeReasonTag = localWakeReasonTag; + wakeReasonTag.readFromParcel(src); + } else { + wakeReasonTag = null; + } + if ((bat&0x40000000) != 0) { + eventCode = src.readInt(); + eventTag = localEventTag; + eventTag.readFromParcel(src); + } else { + eventCode = EVENT_NONE; + eventTag = null; + } + if (cmd == CMD_CURRENT_TIME || cmd == CMD_RESET) { + currentTime = src.readLong(); + } else { + currentTime = 0; + } + numReadInts += (src.dataPosition()-start)/4; + } + + public void clear() { + time = 0; + cmd = CMD_NULL; + batteryLevel = 0; + batteryStatus = 0; + batteryHealth = 0; + batteryPlugType = 0; + batteryTemperature = 0; + batteryVoltage = 0; + states = 0; + states2 = 0; + wakelockTag = null; + wakeReasonTag = null; + eventCode = EVENT_NONE; + eventTag = null; + } + + public void setTo(HistoryItem o) { + time = o.time; + cmd = o.cmd; + setToCommon(o); + } + + public void setTo(long time, byte cmd, HistoryItem o) { + this.time = time; + this.cmd = cmd; + setToCommon(o); + } + + private void setToCommon(HistoryItem o) { + batteryLevel = o.batteryLevel; + batteryStatus = o.batteryStatus; + batteryHealth = o.batteryHealth; + batteryPlugType = o.batteryPlugType; + batteryTemperature = o.batteryTemperature; + batteryVoltage = o.batteryVoltage; + states = o.states; + states2 = o.states2; + if (o.wakelockTag != null) { + wakelockTag = localWakelockTag; + wakelockTag.setTo(o.wakelockTag); + } else { + wakelockTag = null; + } + if (o.wakeReasonTag != null) { + wakeReasonTag = localWakeReasonTag; + wakeReasonTag.setTo(o.wakeReasonTag); + } else { + wakeReasonTag = null; + } + eventCode = o.eventCode; + if (o.eventTag != null) { + eventTag = localEventTag; + eventTag.setTo(o.eventTag); + } else { + eventTag = null; + } + currentTime = o.currentTime; + } + + public boolean sameNonEvent(HistoryItem o) { + return batteryLevel == o.batteryLevel + && batteryStatus == o.batteryStatus + && batteryHealth == o.batteryHealth + && batteryPlugType == o.batteryPlugType + && batteryTemperature == o.batteryTemperature + && batteryVoltage == o.batteryVoltage + && states == o.states + && states2 == o.states2 + && currentTime == o.currentTime; + } + + public boolean same(HistoryItem o) { + if (!sameNonEvent(o) || eventCode != o.eventCode) { + return false; + } + if (wakelockTag != o.wakelockTag) { + if (wakelockTag == null || o.wakelockTag == null) { + return false; + } + if (!wakelockTag.equals(o.wakelockTag)) { + return false; + } + } + if (wakeReasonTag != o.wakeReasonTag) { + if (wakeReasonTag == null || o.wakeReasonTag == null) { + return false; + } + if (!wakeReasonTag.equals(o.wakeReasonTag)) { + return false; + } + } + if (eventTag != o.eventTag) { + if (eventTag == null || o.eventTag == null) { + return false; + } + if (!eventTag.equals(o.eventTag)) { + return false; + } + } + return true; + } + } + + public final static class HistoryEventTracker { + private final HashMap[] mActiveEvents + = (HashMap[]) new HashMap[HistoryItem.EVENT_COUNT]; + + public boolean updateState(int code, String name, int uid, int poolIdx) { + if ((code&HistoryItem.EVENT_FLAG_START) != 0) { + int idx = code&HistoryItem.EVENT_TYPE_MASK; + HashMap active = mActiveEvents[idx]; + if (active == null) { + active = new HashMap(); + mActiveEvents[idx] = active; + } + SparseIntArray uids = active.get(name); + if (uids == null) { + uids = new SparseIntArray(); + active.put(name, uids); + } + if (uids.indexOfKey(uid) >= 0) { + // Already set, nothing to do! + return false; + } + uids.put(uid, poolIdx); + } else if ((code&HistoryItem.EVENT_FLAG_FINISH) != 0) { + int idx = code&HistoryItem.EVENT_TYPE_MASK; + HashMap active = mActiveEvents[idx]; + if (active == null) { + // not currently active, nothing to do. + return false; + } + SparseIntArray uids = active.get(name); + if (uids == null) { + // not currently active, nothing to do. + return false; + } + idx = uids.indexOfKey(uid); + if (idx < 0) { + // not currently active, nothing to do. + return false; + } + uids.removeAt(idx); + if (uids.size() <= 0) { + active.remove(name); + } + } + return true; + } + + public void removeEvents(int code) { + int idx = code&HistoryItem.EVENT_TYPE_MASK; + mActiveEvents[idx] = null; + } + + public HashMap getStateForEvent(int code) { + return mActiveEvents[code]; + } + } + + public static final class BitDescription { + public final int mask; + public final int shift; + public final String name; + public final String shortName; + public final String[] values; + public final String[] shortValues; + + public BitDescription(int mask, String name, String shortName) { + this.mask = mask; + this.shift = -1; + this.name = name; + this.shortName = shortName; + this.values = null; + this.shortValues = null; + } + + public BitDescription(int mask, int shift, String name, String shortName, + String[] values, String[] shortValues) { + this.mask = mask; + this.shift = shift; + this.name = name; + this.shortName = shortName; + this.values = values; + this.shortValues = shortValues; + } + } + + /** + * Don't allow any more batching in to the current history event. This + * is called when printing partial histories, so to ensure that the next + * history event will go in to a new batch after what was printed in the + * last partial history. + */ + public abstract void commitCurrentHistoryBatchLocked(); + + public abstract int getHistoryTotalSize(); + + public abstract int getHistoryUsedSize(); + + public abstract boolean startIteratingHistoryLocked(); + + public abstract int getHistoryStringPoolSize(); + + public abstract int getHistoryStringPoolBytes(); + + public abstract String getHistoryTagPoolString(int index); + + public abstract int getHistoryTagPoolUid(int index); + + public abstract boolean getNextHistoryLocked(HistoryItem out); + + public abstract void finishIteratingHistoryLocked(); + + public abstract boolean startIteratingOldHistoryLocked(); + + public abstract boolean getNextOldHistoryLocked(HistoryItem out); + + public abstract void finishIteratingOldHistoryLocked(); + + /** + * Return the base time offset for the battery history. + */ + public abstract long getHistoryBaseTime(); + + /** + * Returns the number of times the device has been started. + */ + public abstract int getStartCount(); + + /** + * Returns the time in microseconds that the screen has been on while the device was + * running on battery. + * + * {@hide} + */ + public abstract long getScreenOnTime(long elapsedRealtimeUs, int which); + + /** + * Returns the number of times the screen was turned on. + * + * {@hide} + */ + public abstract int getScreenOnCount(int which); + + public abstract long getInteractiveTime(long elapsedRealtimeUs, int which); + + public static final int SCREEN_BRIGHTNESS_DARK = 0; + public static final int SCREEN_BRIGHTNESS_DIM = 1; + public static final int SCREEN_BRIGHTNESS_MEDIUM = 2; + public static final int SCREEN_BRIGHTNESS_LIGHT = 3; + public static final int SCREEN_BRIGHTNESS_BRIGHT = 4; + + static final String[] SCREEN_BRIGHTNESS_NAMES = { + "dark", "dim", "medium", "light", "bright" + }; + + static final String[] SCREEN_BRIGHTNESS_SHORT_NAMES = { + "0", "1", "2", "3", "4" + }; + + public static final int NUM_SCREEN_BRIGHTNESS_BINS = 5; + + /** + * Returns the time in microseconds that the screen has been on with + * the given brightness + * + * {@hide} + */ + public abstract long getScreenBrightnessTime(int brightnessBin, + long elapsedRealtimeUs, int which); + + /** + * Returns the time in microseconds that low power mode has been enabled while the device was + * running on battery. + * + * {@hide} + */ + public abstract long getLowPowerModeEnabledTime(long elapsedRealtimeUs, int which); + + /** + * Returns the number of times that low power mode was enabled. + * + * {@hide} + */ + public abstract int getLowPowerModeEnabledCount(int which); + + /** + * Returns the number of times that connectivity state changed. + * + * {@hide} + */ + public abstract int getNumConnectivityChange(int which); + + /** + * Returns the time in microseconds that the phone has been on while the device was + * running on battery. + * + * {@hide} + */ + public abstract long getPhoneOnTime(long elapsedRealtimeUs, int which); + + /** + * Returns the number of times a phone call was activated. + * + * {@hide} + */ + public abstract int getPhoneOnCount(int which); + + /** + * Returns the time in microseconds that the phone has been running with + * the given signal strength. + * + * {@hide} + */ + public abstract long getPhoneSignalStrengthTime(int strengthBin, + long elapsedRealtimeUs, int which); + + /** + * Returns the time in microseconds that the phone has been trying to + * acquire a signal. + * + * {@hide} + */ + public abstract long getPhoneSignalScanningTime( + long elapsedRealtimeUs, int which); + + /** + * Returns the number of times the phone has entered the given signal strength. + * + * {@hide} + */ + public abstract int getPhoneSignalStrengthCount(int strengthBin, int which); + + /** + * Returns the time in microseconds that the mobile network has been active + * (in a high power state). + * + * {@hide} + */ + public abstract long getMobileRadioActiveTime(long elapsedRealtimeUs, int which); + + /** + * Returns the number of times that the mobile network has transitioned to the + * active state. + * + * {@hide} + */ + public abstract int getMobileRadioActiveCount(int which); + + /** + * Returns the time in microseconds that is the difference between the mobile radio + * time we saw based on the elapsed timestamp when going down vs. the given time stamp + * from the radio. + * + * {@hide} + */ + public abstract long getMobileRadioActiveAdjustedTime(int which); + + /** + * Returns the time in microseconds that the mobile network has been active + * (in a high power state) but not being able to blame on an app. + * + * {@hide} + */ + public abstract long getMobileRadioActiveUnknownTime(int which); + + /** + * Return count of number of times radio was up that could not be blamed on apps. + * + * {@hide} + */ + public abstract int getMobileRadioActiveUnknownCount(int which); + + public static final int DATA_CONNECTION_NONE = 0; + public static final int DATA_CONNECTION_GPRS = 1; + public static final int DATA_CONNECTION_EDGE = 2; + public static final int DATA_CONNECTION_UMTS = 3; + public static final int DATA_CONNECTION_CDMA = 4; + public static final int DATA_CONNECTION_EVDO_0 = 5; + public static final int DATA_CONNECTION_EVDO_A = 6; + public static final int DATA_CONNECTION_1xRTT = 7; + public static final int DATA_CONNECTION_HSDPA = 8; + public static final int DATA_CONNECTION_HSUPA = 9; + public static final int DATA_CONNECTION_HSPA = 10; + public static final int DATA_CONNECTION_IDEN = 11; + public static final int DATA_CONNECTION_EVDO_B = 12; + public static final int DATA_CONNECTION_LTE = 13; + public static final int DATA_CONNECTION_EHRPD = 14; + public static final int DATA_CONNECTION_HSPAP = 15; + public static final int DATA_CONNECTION_OTHER = 16; + + static final String[] DATA_CONNECTION_NAMES = { + "none", "gprs", "edge", "umts", "cdma", "evdo_0", "evdo_A", + "1xrtt", "hsdpa", "hsupa", "hspa", "iden", "evdo_b", "lte", + "ehrpd", "hspap", "other" + }; + + public static final int NUM_DATA_CONNECTION_TYPES = DATA_CONNECTION_OTHER+1; + + /** + * Returns the time in microseconds that the phone has been running with + * the given data connection. + * + * {@hide} + */ + public abstract long getPhoneDataConnectionTime(int dataType, + long elapsedRealtimeUs, int which); + + /** + * Returns the number of times the phone has entered the given data + * connection type. + * + * {@hide} + */ + public abstract int getPhoneDataConnectionCount(int dataType, int which); + + public static final int WIFI_SUPPL_STATE_INVALID = 0; + public static final int WIFI_SUPPL_STATE_DISCONNECTED = 1; + public static final int WIFI_SUPPL_STATE_INTERFACE_DISABLED = 2; + public static final int WIFI_SUPPL_STATE_INACTIVE = 3; + public static final int WIFI_SUPPL_STATE_SCANNING = 4; + public static final int WIFI_SUPPL_STATE_AUTHENTICATING = 5; + public static final int WIFI_SUPPL_STATE_ASSOCIATING = 6; + public static final int WIFI_SUPPL_STATE_ASSOCIATED = 7; + public static final int WIFI_SUPPL_STATE_FOUR_WAY_HANDSHAKE = 8; + public static final int WIFI_SUPPL_STATE_GROUP_HANDSHAKE = 9; + public static final int WIFI_SUPPL_STATE_COMPLETED = 10; + public static final int WIFI_SUPPL_STATE_DORMANT = 11; + public static final int WIFI_SUPPL_STATE_UNINITIALIZED = 12; + + public static final int NUM_WIFI_SUPPL_STATES = WIFI_SUPPL_STATE_UNINITIALIZED+1; + + static final String[] WIFI_SUPPL_STATE_NAMES = { + "invalid", "disconn", "disabled", "inactive", "scanning", + "authenticating", "associating", "associated", "4-way-handshake", + "group-handshake", "completed", "dormant", "uninit" + }; + + static final String[] WIFI_SUPPL_STATE_SHORT_NAMES = { + "inv", "dsc", "dis", "inact", "scan", + "auth", "ascing", "asced", "4-way", + "group", "compl", "dorm", "uninit" + }; + + public static final BitDescription[] HISTORY_STATE_DESCRIPTIONS + = new BitDescription[] { + new BitDescription(HistoryItem.STATE_CPU_RUNNING_FLAG, "running", "r"), + new BitDescription(HistoryItem.STATE_WAKE_LOCK_FLAG, "wake_lock", "w"), + new BitDescription(HistoryItem.STATE_SENSOR_ON_FLAG, "sensor", "s"), + new BitDescription(HistoryItem.STATE_GPS_ON_FLAG, "gps", "g"), + new BitDescription(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG, "wifi_full_lock", "Wl"), + new BitDescription(HistoryItem.STATE_WIFI_SCAN_FLAG, "wifi_scan", "Ws"), + new BitDescription(HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG, "wifi_multicast", "Wm"), + new BitDescription(HistoryItem.STATE_MOBILE_RADIO_ACTIVE_FLAG, "mobile_radio", "Pr"), + new BitDescription(HistoryItem.STATE_PHONE_SCANNING_FLAG, "phone_scanning", "Psc"), + new BitDescription(HistoryItem.STATE_AUDIO_ON_FLAG, "audio", "a"), + new BitDescription(HistoryItem.STATE_SCREEN_ON_FLAG, "screen", "S"), + new BitDescription(HistoryItem.STATE_BATTERY_PLUGGED_FLAG, "plugged", "BP"), + new BitDescription(HistoryItem.STATE_PHONE_IN_CALL_FLAG, "phone_in_call", "Pcl"), + new BitDescription(HistoryItem.STATE_BLUETOOTH_ON_FLAG, "bluetooth", "b"), + new BitDescription(HistoryItem.STATE_DATA_CONNECTION_MASK, + HistoryItem.STATE_DATA_CONNECTION_SHIFT, "data_conn", "Pcn", + DATA_CONNECTION_NAMES, DATA_CONNECTION_NAMES), + new BitDescription(HistoryItem.STATE_PHONE_STATE_MASK, + HistoryItem.STATE_PHONE_STATE_SHIFT, "phone_state", "Pst", + new String[] {"in", "out", "emergency", "off"}, + new String[] {"in", "out", "em", "off"}), + new BitDescription(HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK, + HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT, "phone_signal_strength", "Pss", + SignalStrength.SIGNAL_STRENGTH_NAMES, + new String[] { "0", "1", "2", "3", "4" }), + new BitDescription(HistoryItem.STATE_BRIGHTNESS_MASK, + HistoryItem.STATE_BRIGHTNESS_SHIFT, "brightness", "Sb", + SCREEN_BRIGHTNESS_NAMES, SCREEN_BRIGHTNESS_SHORT_NAMES), + }; + + public static final BitDescription[] HISTORY_STATE2_DESCRIPTIONS + = new BitDescription[] { + new BitDescription(HistoryItem.STATE2_LOW_POWER_FLAG, "low_power", "lp"), + new BitDescription(HistoryItem.STATE2_VIDEO_ON_FLAG, "video", "v"), + new BitDescription(HistoryItem.STATE2_WIFI_RUNNING_FLAG, "wifi_running", "Wr"), + new BitDescription(HistoryItem.STATE2_WIFI_ON_FLAG, "wifi", "W"), + new BitDescription(HistoryItem.STATE2_FLASHLIGHT_FLAG, "flashlight", "fl"), + new BitDescription(HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK, + HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT, "wifi_signal_strength", "Wss", + new String[] { "0", "1", "2", "3", "4" }, + new String[] { "0", "1", "2", "3", "4" }), + new BitDescription(HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK, + HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT, "wifi_suppl", "Wsp", + WIFI_SUPPL_STATE_NAMES, WIFI_SUPPL_STATE_SHORT_NAMES), + }; + + public static final String[] HISTORY_EVENT_NAMES = new String[] { + "null", "proc", "fg", "top", "sync", "wake_lock_in", "job", "user", "userfg", "conn" + }; + + public static final String[] HISTORY_EVENT_CHECKIN_NAMES = new String[] { + "Enl", "Epr", "Efg", "Etp", "Esy", "Ewl", "Ejb", "Eur", "Euf", "Ecn" + }; + + /** + * Returns the time in microseconds that wifi has been on while the device was + * running on battery. + * + * {@hide} + */ + public abstract long getWifiOnTime(long elapsedRealtimeUs, int which); + + /** + * Returns the time in microseconds that wifi has been on and the driver has + * been in the running state while the device was running on battery. + * + * {@hide} + */ + public abstract long getGlobalWifiRunningTime(long elapsedRealtimeUs, int which); + + public static final int WIFI_STATE_OFF = 0; + public static final int WIFI_STATE_OFF_SCANNING = 1; + public static final int WIFI_STATE_ON_NO_NETWORKS = 2; + public static final int WIFI_STATE_ON_DISCONNECTED = 3; + public static final int WIFI_STATE_ON_CONNECTED_STA = 4; + public static final int WIFI_STATE_ON_CONNECTED_P2P = 5; + public static final int WIFI_STATE_ON_CONNECTED_STA_P2P = 6; + public static final int WIFI_STATE_SOFT_AP = 7; + + static final String[] WIFI_STATE_NAMES = { + "off", "scanning", "no_net", "disconn", + "sta", "p2p", "sta_p2p", "soft_ap" + }; + + public static final int NUM_WIFI_STATES = WIFI_STATE_SOFT_AP+1; + + /** + * Returns the time in microseconds that WiFi has been running in the given state. + * + * {@hide} + */ + public abstract long getWifiStateTime(int wifiState, + long elapsedRealtimeUs, int which); + + /** + * Returns the number of times that WiFi has entered the given state. + * + * {@hide} + */ + public abstract int getWifiStateCount(int wifiState, int which); + + /** + * Returns the time in microseconds that the wifi supplicant has been + * in a given state. + * + * {@hide} + */ + public abstract long getWifiSupplStateTime(int state, long elapsedRealtimeUs, int which); + + /** + * Returns the number of times that the wifi supplicant has transitioned + * to a given state. + * + * {@hide} + */ + public abstract int getWifiSupplStateCount(int state, int which); + + public static final int NUM_WIFI_SIGNAL_STRENGTH_BINS = 5; + + /** + * Returns the time in microseconds that WIFI has been running with + * the given signal strength. + * + * {@hide} + */ + public abstract long getWifiSignalStrengthTime(int strengthBin, + long elapsedRealtimeUs, int which); + + /** + * Returns the number of times WIFI has entered the given signal strength. + * + * {@hide} + */ + public abstract int getWifiSignalStrengthCount(int strengthBin, int which); + + /** + * Returns the time in microseconds that bluetooth has been on while the device was + * running on battery. + * + * {@hide} + */ + public abstract long getBluetoothOnTime(long elapsedRealtimeUs, int which); + + public abstract int getBluetoothPingCount(); + + public static final int BLUETOOTH_STATE_INACTIVE = 0; + public static final int BLUETOOTH_STATE_LOW = 1; + public static final int BLUETOOTH_STATE_MEDIUM = 2; + public static final int BLUETOOTH_STATE_HIGH = 3; + + static final String[] BLUETOOTH_STATE_NAMES = { + "inactive", "low", "med", "high" + }; + + public static final int NUM_BLUETOOTH_STATES = BLUETOOTH_STATE_HIGH +1; + + /** + * Returns the time in microseconds that Bluetooth has been running in the + * given active state. + * + * {@hide} + */ + public abstract long getBluetoothStateTime(int bluetoothState, + long elapsedRealtimeUs, int which); + + /** + * Returns the number of times that Bluetooth has entered the given active state. + * + * {@hide} + */ + public abstract int getBluetoothStateCount(int bluetoothState, int which); + + /** + * Returns the time in microseconds that the flashlight has been on while the device was + * running on battery. + * + * {@hide} + */ + public abstract long getFlashlightOnTime(long elapsedRealtimeUs, int which); + + /** + * Returns the number of times that the flashlight has been turned on while the device was + * running on battery. + * + * {@hide} + */ + public abstract long getFlashlightOnCount(int which); + + public static final int NETWORK_MOBILE_RX_DATA = 0; + public static final int NETWORK_MOBILE_TX_DATA = 1; + public static final int NETWORK_WIFI_RX_DATA = 2; + public static final int NETWORK_WIFI_TX_DATA = 3; + + public static final int NUM_NETWORK_ACTIVITY_TYPES = NETWORK_WIFI_TX_DATA + 1; + + public abstract long getNetworkActivityBytes(int type, int which); + public abstract long getNetworkActivityPackets(int type, int which); + + /** + * Return the wall clock time when battery stats data collection started. + */ + public abstract long getStartClockTime(); + + /** + * Return platform version tag that we were running in when the battery stats started. + */ + public abstract String getStartPlatformVersion(); + + /** + * Return platform version tag that we were running in when the battery stats ended. + */ + public abstract String getEndPlatformVersion(); + + /** + * Return the internal version code of the parcelled format. + */ + public abstract int getParcelVersion(); + + /** + * Return whether we are currently running on battery. + */ + public abstract boolean getIsOnBattery(); + + /** + * Returns a SparseArray containing the statistics for each uid. + */ + public abstract SparseArray getUidStats(); + + /** + * Returns the current battery uptime in microseconds. + * + * @param curTime the amount of elapsed realtime in microseconds. + */ + public abstract long getBatteryUptime(long curTime); + + /** + * Returns the current battery realtime in microseconds. + * + * @param curTime the amount of elapsed realtime in microseconds. + */ + public abstract long getBatteryRealtime(long curTime); + + /** + * Returns the battery percentage level at the last time the device was unplugged from power, or + * the last time it booted on battery power. + */ + public abstract int getDischargeStartLevel(); + + /** + * Returns the current battery percentage level if we are in a discharge cycle, otherwise + * returns the level at the last plug event. + */ + public abstract int getDischargeCurrentLevel(); + + /** + * Get the amount the battery has discharged since the stats were + * last reset after charging, as a lower-end approximation. + */ + public abstract int getLowDischargeAmountSinceCharge(); + + /** + * Get the amount the battery has discharged since the stats were + * last reset after charging, as an upper-end approximation. + */ + public abstract int getHighDischargeAmountSinceCharge(); + + /** + * Retrieve the discharge amount over the selected discharge period which. + */ + public abstract int getDischargeAmount(int which); + + /** + * Get the amount the battery has discharged while the screen was on, + * since the last time power was unplugged. + */ + public abstract int getDischargeAmountScreenOn(); + + /** + * Get the amount the battery has discharged while the screen was on, + * since the last time the device was charged. + */ + public abstract int getDischargeAmountScreenOnSinceCharge(); + + /** + * Get the amount the battery has discharged while the screen was off, + * since the last time power was unplugged. + */ + public abstract int getDischargeAmountScreenOff(); + + /** + * Get the amount the battery has discharged while the screen was off, + * since the last time the device was charged. + */ + public abstract int getDischargeAmountScreenOffSinceCharge(); + + /** + * Returns the total, last, or current battery uptime in microseconds. + * + * @param curTime the elapsed realtime in microseconds. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract long computeBatteryUptime(long curTime, int which); + + /** + * Returns the total, last, or current battery realtime in microseconds. + * + * @param curTime the current elapsed realtime in microseconds. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract long computeBatteryRealtime(long curTime, int which); + + /** + * Returns the total, last, or current battery screen off uptime in microseconds. + * + * @param curTime the elapsed realtime in microseconds. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract long computeBatteryScreenOffUptime(long curTime, int which); + + /** + * Returns the total, last, or current battery screen off realtime in microseconds. + * + * @param curTime the current elapsed realtime in microseconds. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract long computeBatteryScreenOffRealtime(long curTime, int which); + + /** + * Returns the total, last, or current uptime in microseconds. + * + * @param curTime the current elapsed realtime in microseconds. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract long computeUptime(long curTime, int which); + + /** + * Returns the total, last, or current realtime in microseconds. + * + * @param curTime the current elapsed realtime in microseconds. + * @param which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + */ + public abstract long computeRealtime(long curTime, int which); + + /** + * Compute an approximation for how much run time (in microseconds) is remaining on + * the battery. Returns -1 if no time can be computed: either there is not + * enough current data to make a decision, or the battery is currently + * charging. + * + * @param curTime The current elepsed realtime in microseconds. + */ + public abstract long computeBatteryTimeRemaining(long curTime); + + // The part of a step duration that is the actual time. + public static final long STEP_LEVEL_TIME_MASK = 0x000000ffffffffffL; + + // Bits in a step duration that are the new battery level we are at. + public static final long STEP_LEVEL_LEVEL_MASK = 0x0000ff0000000000L; + public static final long STEP_LEVEL_LEVEL_SHIFT = 40; + + // Bits in a step duration that are the initial mode we were in at that step. + public static final long STEP_LEVEL_INITIAL_MODE_MASK = 0x00ff000000000000L; + public static final long STEP_LEVEL_INITIAL_MODE_SHIFT = 48; + + // Bits in a step duration that indicate which modes changed during that step. + public static final long STEP_LEVEL_MODIFIED_MODE_MASK = 0xff00000000000000L; + public static final long STEP_LEVEL_MODIFIED_MODE_SHIFT = 56; + + // Step duration mode: the screen is on, off, dozed, etc; value is Display.STATE_* - 1. + public static final int STEP_LEVEL_MODE_SCREEN_STATE = 0x03; + + // Step duration mode: power save is on. + public static final int STEP_LEVEL_MODE_POWER_SAVE = 0x04; + + /** + * Return the historical number of discharge steps we currently have. + */ + public abstract int getNumDischargeStepDurations(); + + /** + * Return the array of discharge step durations; the number of valid + * items in it is returned by {@link #getNumDischargeStepDurations()}. + * These values are in milliseconds. + */ + public abstract long[] getDischargeStepDurationsArray(); + + /** + * Compute an approximation for how much time (in microseconds) remains until the battery + * is fully charged. Returns -1 if no time can be computed: either there is not + * enough current data to make a decision, or the battery is currently + * discharging. + * + * @param curTime The current elepsed realtime in microseconds. + */ + public abstract long computeChargeTimeRemaining(long curTime); + + /** + * Return the historical number of charge steps we currently have. + */ + public abstract int getNumChargeStepDurations(); + + /** + * Return the array of charge step durations; the number of valid + * items in it is returned by {@link #getNumChargeStepDurations()}. + * These values are in milliseconds. + */ + public abstract long[] getChargeStepDurationsArray(); + + public abstract Map getWakeupReasonStats(); + + public abstract Map getKernelWakelockStats(); + + /** Returns the number of different speeds that the CPU can run at */ + public abstract int getCpuSpeedSteps(); + + public abstract void writeToParcelWithoutUids(Parcel out, int flags); + + private final static void formatTimeRaw(StringBuilder out, long seconds) { + long days = seconds / (60 * 60 * 24); + if (days != 0) { + out.append(days); + out.append("d "); + } + long used = days * 60 * 60 * 24; + + long hours = (seconds - used) / (60 * 60); + if (hours != 0 || used != 0) { + out.append(hours); + out.append("h "); + } + used += hours * 60 * 60; + + long mins = (seconds-used) / 60; + if (mins != 0 || used != 0) { + out.append(mins); + out.append("m "); + } + used += mins * 60; + + if (seconds != 0 || used != 0) { + out.append(seconds-used); + out.append("s "); + } + } + + public final static void formatTime(StringBuilder sb, long time) { + long sec = time / 100; + formatTimeRaw(sb, sec); + sb.append((time - (sec * 100)) * 10); + sb.append("ms "); + } + + public final static void formatTimeMs(StringBuilder sb, long time) { + long sec = time / 1000; + formatTimeRaw(sb, sec); + sb.append(time - (sec * 1000)); + sb.append("ms "); + } + + public final static void formatTimeMsNoSpace(StringBuilder sb, long time) { + long sec = time / 1000; + formatTimeRaw(sb, sec); + sb.append(time - (sec * 1000)); + sb.append("ms"); + } + + public final String formatRatioLocked(long num, long den) { + if (den == 0L) { + return "--%"; + } + float perc = ((float)num) / ((float)den) * 100; + mFormatBuilder.setLength(0); + mFormatter.format("%.1f%%", perc); + return mFormatBuilder.toString(); + } + + final String formatBytesLocked(long bytes) { + mFormatBuilder.setLength(0); + + if (bytes < BYTES_PER_KB) { + return bytes + "B"; + } else if (bytes < BYTES_PER_MB) { + mFormatter.format("%.2fKB", bytes / (double) BYTES_PER_KB); + return mFormatBuilder.toString(); + } else if (bytes < BYTES_PER_GB){ + mFormatter.format("%.2fMB", bytes / (double) BYTES_PER_MB); + return mFormatBuilder.toString(); + } else { + mFormatter.format("%.2fGB", bytes / (double) BYTES_PER_GB); + return mFormatBuilder.toString(); + } + } + + private static long computeWakeLock(Timer timer, long elapsedRealtimeUs, int which) { + if (timer != null) { + // Convert from microseconds to milliseconds with rounding + long totalTimeMicros = timer.getTotalTimeLocked(elapsedRealtimeUs, which); + long totalTimeMillis = (totalTimeMicros + 500) / 1000; + return totalTimeMillis; + } + return 0; + } + + /** + * + * @param sb a StringBuilder object. + * @param timer a Timer object contining the wakelock times. + * @param elapsedRealtimeUs the current on-battery time in microseconds. + * @param name the name of the wakelock. + * @param which which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + * @param linePrefix a String to be prepended to each line of output. + * @return the line prefix + */ + private static final String printWakeLock(StringBuilder sb, Timer timer, + long elapsedRealtimeUs, String name, int which, String linePrefix) { + + if (timer != null) { + long totalTimeMillis = computeWakeLock(timer, elapsedRealtimeUs, which); + + int count = timer.getCountLocked(which); + if (totalTimeMillis != 0) { + sb.append(linePrefix); + formatTimeMs(sb, totalTimeMillis); + if (name != null) { + sb.append(name); + sb.append(' '); + } + sb.append('('); + sb.append(count); + sb.append(" times)"); + return ", "; + } + } + return linePrefix; + } + + /** + * Checkin version of wakelock printer. Prints simple comma-separated list. + * + * @param sb a StringBuilder object. + * @param timer a Timer object contining the wakelock times. + * @param elapsedRealtimeUs the current time in microseconds. + * @param name the name of the wakelock. + * @param which which one of STATS_SINCE_CHARGED, STATS_SINCE_UNPLUGGED, or STATS_CURRENT. + * @param linePrefix a String to be prepended to each line of output. + * @return the line prefix + */ + private static final String printWakeLockCheckin(StringBuilder sb, Timer timer, + long elapsedRealtimeUs, String name, int which, String linePrefix) { + long totalTimeMicros = 0; + int count = 0; + if (timer != null) { + totalTimeMicros = timer.getTotalTimeLocked(elapsedRealtimeUs, which); + count = timer.getCountLocked(which); + } + sb.append(linePrefix); + sb.append((totalTimeMicros + 500) / 1000); // microseconds to milliseconds with rounding + sb.append(','); + sb.append(name != null ? name + "," : ""); + sb.append(count); + return ","; + } + + /** + * Dump a comma-separated line of values for terse checkin mode. + * + * @param pw the PageWriter to dump log to + * @param category category of data (e.g. "total", "last", "unplugged", "current" ) + * @param type type of data (e.g. "wakelock", "sensor", "process", "apk" , "process", "network") + * @param args type-dependent data arguments + */ + private static final void dumpLine(PrintWriter pw, int uid, String category, String type, + Object... args ) { + pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); + pw.print(uid); pw.print(','); + pw.print(category); pw.print(','); + pw.print(type); + + for (Object arg : args) { + pw.print(','); + pw.print(arg); + } + pw.println(); + } + + /** + * Temporary for settings. + */ + public final void dumpCheckinLocked(Context context, PrintWriter pw, int which, int reqUid) { + dumpCheckinLocked(context, pw, which, reqUid, BatteryStatsHelper.checkWifiOnly(context)); + } + + /** + * Checkin server version of dump to produce more compact, computer-readable log. + * + * NOTE: all times are expressed in 'ms'. + */ + public final void dumpCheckinLocked(Context context, PrintWriter pw, int which, int reqUid, + boolean wifiOnly) { + final long rawUptime = SystemClock.uptimeMillis() * 1000; + final long rawRealtime = SystemClock.elapsedRealtime() * 1000; + final long batteryUptime = getBatteryUptime(rawUptime); + final long whichBatteryUptime = computeBatteryUptime(rawUptime, which); + final long whichBatteryRealtime = computeBatteryRealtime(rawRealtime, which); + final long whichBatteryScreenOffUptime = computeBatteryScreenOffUptime(rawUptime, which); + final long whichBatteryScreenOffRealtime = computeBatteryScreenOffRealtime(rawRealtime, + which); + final long totalRealtime = computeRealtime(rawRealtime, which); + final long totalUptime = computeUptime(rawUptime, which); + final long screenOnTime = getScreenOnTime(rawRealtime, which); + final long interactiveTime = getInteractiveTime(rawRealtime, which); + final long lowPowerModeEnabledTime = getLowPowerModeEnabledTime(rawRealtime, which); + final int connChanges = getNumConnectivityChange(which); + final long phoneOnTime = getPhoneOnTime(rawRealtime, which); + final long wifiOnTime = getWifiOnTime(rawRealtime, which); + final long wifiRunningTime = getGlobalWifiRunningTime(rawRealtime, which); + final long bluetoothOnTime = getBluetoothOnTime(rawRealtime, which); + + StringBuilder sb = new StringBuilder(128); + + SparseArray uidStats = getUidStats(); + final int NU = uidStats.size(); + + String category = STAT_NAMES[which]; + + // Dump "battery" stat + dumpLine(pw, 0 /* uid */, category, BATTERY_DATA, + which == STATS_SINCE_CHARGED ? getStartCount() : "N/A", + whichBatteryRealtime / 1000, whichBatteryUptime / 1000, + totalRealtime / 1000, totalUptime / 1000, + getStartClockTime(), + whichBatteryScreenOffRealtime / 1000, whichBatteryScreenOffUptime / 1000); + + // Calculate wakelock times across all uids. + long fullWakeLockTimeTotal = 0; + long partialWakeLockTimeTotal = 0; + + for (int iu = 0; iu < NU; iu++) { + Uid u = uidStats.valueAt(iu); + + Map wakelocks = u.getWakelockStats(); + if (wakelocks.size() > 0) { + for (Map.Entry ent + : wakelocks.entrySet()) { + Uid.Wakelock wl = ent.getValue(); + + Timer fullWakeTimer = wl.getWakeTime(WAKE_TYPE_FULL); + if (fullWakeTimer != null) { + fullWakeLockTimeTotal += fullWakeTimer.getTotalTimeLocked(rawRealtime, + which); + } + + Timer partialWakeTimer = wl.getWakeTime(WAKE_TYPE_PARTIAL); + if (partialWakeTimer != null) { + partialWakeLockTimeTotal += partialWakeTimer.getTotalTimeLocked( + rawRealtime, which); + } + } + } + } + + long mobileRxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which); + long mobileTxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which); + long wifiRxTotalBytes = getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which); + long wifiTxTotalBytes = getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which); + long mobileRxTotalPackets = getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which); + long mobileTxTotalPackets = getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which); + long wifiRxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which); + long wifiTxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which); + + // Dump network stats + dumpLine(pw, 0 /* uid */, category, GLOBAL_NETWORK_DATA, + mobileRxTotalBytes, mobileTxTotalBytes, wifiRxTotalBytes, wifiTxTotalBytes, + mobileRxTotalPackets, mobileTxTotalPackets, wifiRxTotalPackets, wifiTxTotalPackets); + + // Dump misc stats + dumpLine(pw, 0 /* uid */, category, MISC_DATA, + screenOnTime / 1000, phoneOnTime / 1000, wifiOnTime / 1000, + wifiRunningTime / 1000, bluetoothOnTime / 1000, + mobileRxTotalBytes, mobileTxTotalBytes, wifiRxTotalBytes, wifiTxTotalBytes, + fullWakeLockTimeTotal / 1000, partialWakeLockTimeTotal / 1000, + 0 /*legacy input event count*/, getMobileRadioActiveTime(rawRealtime, which) / 1000, + getMobileRadioActiveAdjustedTime(which) / 1000, interactiveTime / 1000, + lowPowerModeEnabledTime / 1000, connChanges); + + // Dump screen brightness stats + Object[] args = new Object[NUM_SCREEN_BRIGHTNESS_BINS]; + for (int i=0; i kernelWakelocks = getKernelWakelockStats(); + if (kernelWakelocks.size() > 0) { + for (Map.Entry ent : kernelWakelocks.entrySet()) { + sb.setLength(0); + printWakeLockCheckin(sb, ent.getValue(), rawRealtime, null, which, ""); + dumpLine(pw, 0 /* uid */, category, KERNEL_WAKELOCK_DATA, ent.getKey(), + sb.toString()); + } + } + Map wakeupReasons = getWakeupReasonStats(); + if (wakeupReasons.size() > 0) { + for (Map.Entry ent : wakeupReasons.entrySet()) { + // Not doing the regular wake lock formatting to remain compatible + // with the old checkin format. + long totalTimeMicros = ent.getValue().getTotalTimeLocked(rawRealtime, which); + int count = ent.getValue().getCountLocked(which); + dumpLine(pw, 0 /* uid */, category, WAKEUP_REASON_DATA, + "\"" + ent.getKey() + "\"", (totalTimeMicros + 500) / 1000, count); + } + } + } + + BatteryStatsHelper helper = new BatteryStatsHelper(context, false, wifiOnly); + helper.create(this); + helper.refreshStats(which, UserHandle.USER_ALL); + List sippers = helper.getUsageList(); + if (sippers != null && sippers.size() > 0) { + dumpLine(pw, 0 /* uid */, category, POWER_USE_SUMMARY_DATA, + BatteryStatsHelper.makemAh(helper.getPowerProfile().getBatteryCapacity()), + BatteryStatsHelper.makemAh(helper.getComputedPower()), + BatteryStatsHelper.makemAh(helper.getMinDrainedPower()), + BatteryStatsHelper.makemAh(helper.getMaxDrainedPower())); + for (int i=0; i= 0 && uid != reqUid) { + continue; + } + Uid u = uidStats.valueAt(iu); + // Dump Network stats per uid, if any + long mobileBytesRx = u.getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which); + long mobileBytesTx = u.getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which); + long wifiBytesRx = u.getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which); + long wifiBytesTx = u.getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which); + long mobilePacketsRx = u.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which); + long mobilePacketsTx = u.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which); + long mobileActiveTime = u.getMobileRadioActiveTime(which); + int mobileActiveCount = u.getMobileRadioActiveCount(which); + long wifiPacketsRx = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which); + long wifiPacketsTx = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which); + long fullWifiLockOnTime = u.getFullWifiLockTime(rawRealtime, which); + long wifiScanTime = u.getWifiScanTime(rawRealtime, which); + long uidWifiRunningTime = u.getWifiRunningTime(rawRealtime, which); + + if (mobileBytesRx > 0 || mobileBytesTx > 0 || wifiBytesRx > 0 || wifiBytesTx > 0 + || mobilePacketsRx > 0 || mobilePacketsTx > 0 || wifiPacketsRx > 0 + || wifiPacketsTx > 0 || mobileActiveTime > 0 || mobileActiveCount > 0) { + dumpLine(pw, uid, category, NETWORK_DATA, mobileBytesRx, mobileBytesTx, + wifiBytesRx, wifiBytesTx, + mobilePacketsRx, mobilePacketsTx, + wifiPacketsRx, wifiPacketsTx, + mobileActiveTime, mobileActiveCount); + } + + if (fullWifiLockOnTime != 0 || wifiScanTime != 0 + || uidWifiRunningTime != 0) { + dumpLine(pw, uid, category, WIFI_DATA, + fullWifiLockOnTime, wifiScanTime, uidWifiRunningTime); + } + + if (u.hasUserActivity()) { + args = new Object[Uid.NUM_USER_ACTIVITY_TYPES]; + boolean hasData = false; + for (int i=0; i wakelocks = u.getWakelockStats(); + if (wakelocks.size() > 0) { + for (Map.Entry ent : wakelocks.entrySet()) { + Uid.Wakelock wl = ent.getValue(); + String linePrefix = ""; + sb.setLength(0); + linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_FULL), + rawRealtime, "f", which, linePrefix); + linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_PARTIAL), + rawRealtime, "p", which, linePrefix); + linePrefix = printWakeLockCheckin(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), + rawRealtime, "w", which, linePrefix); + + // Only log if we had at lease one wakelock... + if (sb.length() > 0) { + String name = ent.getKey(); + if (name.indexOf(',') >= 0) { + name = name.replace(',', '_'); + } + dumpLine(pw, uid, category, WAKELOCK_DATA, name, sb.toString()); + } + } + } + + Map syncs = u.getSyncStats(); + if (syncs.size() > 0) { + for (Map.Entry ent : syncs.entrySet()) { + Timer timer = ent.getValue(); + // Convert from microseconds to milliseconds with rounding + long totalTime = (timer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; + int count = timer.getCountLocked(which); + if (totalTime != 0) { + dumpLine(pw, uid, category, SYNC_DATA, ent.getKey(), totalTime, count); + } + } + } + + Map jobs = u.getJobStats(); + if (jobs.size() > 0) { + for (Map.Entry ent : jobs.entrySet()) { + Timer timer = ent.getValue(); + // Convert from microseconds to milliseconds with rounding + long totalTime = (timer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; + int count = timer.getCountLocked(which); + if (totalTime != 0) { + dumpLine(pw, uid, category, JOB_DATA, ent.getKey(), totalTime, count); + } + } + } + + SparseArray sensors = u.getSensorStats(); + int NSE = sensors.size(); + for (int ise=0; ise 0) { + dumpLine(pw, uid, category, STATE_TIME_DATA, stateTimes); + } + + Map processStats = u.getProcessStats(); + if (processStats.size() > 0) { + for (Map.Entry ent + : processStats.entrySet()) { + Uid.Proc ps = ent.getValue(); + + final long userMillis = ps.getUserTime(which) * 10; + final long systemMillis = ps.getSystemTime(which) * 10; + final long foregroundMillis = ps.getForegroundTime(which) * 10; + final int starts = ps.getStarts(which); + final int numCrashes = ps.getNumCrashes(which); + final int numAnrs = ps.getNumAnrs(which); + + if (userMillis != 0 || systemMillis != 0 || foregroundMillis != 0 + || starts != 0 || numAnrs != 0 || numCrashes != 0) { + dumpLine(pw, uid, category, PROCESS_DATA, ent.getKey(), userMillis, + systemMillis, foregroundMillis, starts, numAnrs, numCrashes); + } + } + } + + Map packageStats = u.getPackageStats(); + if (packageStats.size() > 0) { + for (Map.Entry ent + : packageStats.entrySet()) { + + Uid.Pkg ps = ent.getValue(); + int wakeups = ps.getWakeups(which); + Map serviceStats = ps.getServiceStats(); + for (Map.Entry sent + : serviceStats.entrySet()) { + BatteryStats.Uid.Pkg.Serv ss = sent.getValue(); + long startTime = ss.getStartTime(batteryUptime, which); + int starts = ss.getStarts(which); + int launches = ss.getLaunches(which); + if (startTime != 0 || starts != 0 || launches != 0) { + dumpLine(pw, uid, category, APK_DATA, + wakeups, // wakeup alarms + ent.getKey(), // Apk + sent.getKey(), // service + startTime / 1000, // time spent started, in ms + starts, + launches); + } + } + } + } + } + } + + static final class TimerEntry { + final String mName; + final int mId; + final BatteryStats.Timer mTimer; + final long mTime; + TimerEntry(String name, int id, BatteryStats.Timer timer, long time) { + mName = name; + mId = id; + mTimer = timer; + mTime = time; + } + } + + private void printmAh(PrintWriter printer, double power) { + printer.print(BatteryStatsHelper.makemAh(power)); + } + + /** + * Temporary for settings. + */ + public final void dumpLocked(Context context, PrintWriter pw, String prefix, int which, + int reqUid) { + dumpLocked(context, pw, prefix, which, reqUid, BatteryStatsHelper.checkWifiOnly(context)); + } + + @SuppressWarnings("unused") + public final void dumpLocked(Context context, PrintWriter pw, String prefix, final int which, + int reqUid, boolean wifiOnly) { + final long rawUptime = SystemClock.uptimeMillis() * 1000; + final long rawRealtime = SystemClock.elapsedRealtime() * 1000; + final long batteryUptime = getBatteryUptime(rawUptime); + + final long whichBatteryUptime = computeBatteryUptime(rawUptime, which); + final long whichBatteryRealtime = computeBatteryRealtime(rawRealtime, which); + final long totalRealtime = computeRealtime(rawRealtime, which); + final long totalUptime = computeUptime(rawUptime, which); + final long whichBatteryScreenOffUptime = computeBatteryScreenOffUptime(rawUptime, which); + final long whichBatteryScreenOffRealtime = computeBatteryScreenOffRealtime(rawRealtime, + which); + final long batteryTimeRemaining = computeBatteryTimeRemaining(rawRealtime); + final long chargeTimeRemaining = computeChargeTimeRemaining(rawRealtime); + + StringBuilder sb = new StringBuilder(128); + + SparseArray uidStats = getUidStats(); + final int NU = uidStats.size(); + + sb.setLength(0); + sb.append(prefix); + sb.append(" Time on battery: "); + formatTimeMs(sb, whichBatteryRealtime / 1000); sb.append("("); + sb.append(formatRatioLocked(whichBatteryRealtime, totalRealtime)); + sb.append(") realtime, "); + formatTimeMs(sb, whichBatteryUptime / 1000); + sb.append("("); sb.append(formatRatioLocked(whichBatteryUptime, totalRealtime)); + sb.append(") uptime"); + pw.println(sb.toString()); + sb.setLength(0); + sb.append(prefix); + sb.append(" Time on battery screen off: "); + formatTimeMs(sb, whichBatteryScreenOffRealtime / 1000); sb.append("("); + sb.append(formatRatioLocked(whichBatteryScreenOffRealtime, totalRealtime)); + sb.append(") realtime, "); + formatTimeMs(sb, whichBatteryScreenOffUptime / 1000); + sb.append("("); + sb.append(formatRatioLocked(whichBatteryScreenOffUptime, totalRealtime)); + sb.append(") uptime"); + pw.println(sb.toString()); + sb.setLength(0); + sb.append(prefix); + sb.append(" Total run time: "); + formatTimeMs(sb, totalRealtime / 1000); + sb.append("realtime, "); + formatTimeMs(sb, totalUptime / 1000); + sb.append("uptime"); + pw.println(sb.toString()); + if (batteryTimeRemaining >= 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Battery time remaining: "); + formatTimeMs(sb, batteryTimeRemaining / 1000); + pw.println(sb.toString()); + } + if (chargeTimeRemaining >= 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Charge time remaining: "); + formatTimeMs(sb, chargeTimeRemaining / 1000); + pw.println(sb.toString()); + } + pw.print(" Start clock time: "); + pw.println(DateFormat.format("yyyy-MM-dd-HH-mm-ss", getStartClockTime()).toString()); + + final long screenOnTime = getScreenOnTime(rawRealtime, which); + final long interactiveTime = getInteractiveTime(rawRealtime, which); + final long lowPowerModeEnabledTime = getLowPowerModeEnabledTime(rawRealtime, which); + final long phoneOnTime = getPhoneOnTime(rawRealtime, which); + final long wifiRunningTime = getGlobalWifiRunningTime(rawRealtime, which); + final long wifiOnTime = getWifiOnTime(rawRealtime, which); + final long bluetoothOnTime = getBluetoothOnTime(rawRealtime, which); + sb.setLength(0); + sb.append(prefix); + sb.append(" Screen on: "); formatTimeMs(sb, screenOnTime / 1000); + sb.append("("); sb.append(formatRatioLocked(screenOnTime, whichBatteryRealtime)); + sb.append(") "); sb.append(getScreenOnCount(which)); + sb.append("x, Interactive: "); formatTimeMs(sb, interactiveTime / 1000); + sb.append("("); sb.append(formatRatioLocked(interactiveTime, whichBatteryRealtime)); + sb.append(")"); + pw.println(sb.toString()); + sb.setLength(0); + sb.append(prefix); + sb.append(" Screen brightnesses:"); + boolean didOne = false; + for (int i=0; i timers = new ArrayList(); + + for (int iu = 0; iu < NU; iu++) { + Uid u = uidStats.valueAt(iu); + + Map wakelocks = u.getWakelockStats(); + if (wakelocks.size() > 0) { + for (Map.Entry ent + : wakelocks.entrySet()) { + Uid.Wakelock wl = ent.getValue(); + + Timer fullWakeTimer = wl.getWakeTime(WAKE_TYPE_FULL); + if (fullWakeTimer != null) { + fullWakeLockTimeTotalMicros += fullWakeTimer.getTotalTimeLocked( + rawRealtime, which); + } + + Timer partialWakeTimer = wl.getWakeTime(WAKE_TYPE_PARTIAL); + if (partialWakeTimer != null) { + long totalTimeMicros = partialWakeTimer.getTotalTimeLocked( + rawRealtime, which); + if (totalTimeMicros > 0) { + if (reqUid < 0) { + // Only show the ordered list of all wake + // locks if the caller is not asking for data + // about a specific uid. + timers.add(new TimerEntry(ent.getKey(), u.getUid(), + partialWakeTimer, totalTimeMicros)); + } + partialWakeLockTimeTotalMicros += totalTimeMicros; + } + } + } + } + } + + long mobileRxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which); + long mobileTxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which); + long wifiRxTotalBytes = getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which); + long wifiTxTotalBytes = getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which); + long mobileRxTotalPackets = getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which); + long mobileTxTotalPackets = getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which); + long wifiRxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which); + long wifiTxTotalPackets = getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which); + + if (fullWakeLockTimeTotalMicros != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Total full wakelock time: "); formatTimeMsNoSpace(sb, + (fullWakeLockTimeTotalMicros + 500) / 1000); + pw.println(sb.toString()); + } + + if (partialWakeLockTimeTotalMicros != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Total partial wakelock time: "); formatTimeMsNoSpace(sb, + (partialWakeLockTimeTotalMicros + 500) / 1000); + pw.println(sb.toString()); + } + + pw.print(prefix); + pw.print(" Mobile total received: "); pw.print(formatBytesLocked(mobileRxTotalBytes)); + pw.print(", sent: "); pw.print(formatBytesLocked(mobileTxTotalBytes)); + pw.print(" (packets received "); pw.print(mobileRxTotalPackets); + pw.print(", sent "); pw.print(mobileTxTotalPackets); pw.println(")"); + sb.setLength(0); + sb.append(prefix); + sb.append(" Phone signal levels:"); + didOne = false; + for (int i=0; i sippers = helper.getUsageList(); + if (sippers != null && sippers.size() > 0) { + pw.print(prefix); pw.println(" Estimated power use (mAh):"); + pw.print(prefix); pw.print(" Capacity: "); + printmAh(pw, helper.getPowerProfile().getBatteryCapacity()); + pw.print(", Computed drain: "); printmAh(pw, helper.getComputedPower()); + pw.print(", actual drain: "); printmAh(pw, helper.getMinDrainedPower()); + if (helper.getMinDrainedPower() != helper.getMaxDrainedPower()) { + pw.print("-"); printmAh(pw, helper.getMaxDrainedPower()); + } + pw.println(); + for (int i=0; i 0) { + pw.print(prefix); pw.println(" Per-app mobile ms per packet:"); + long totalTime = 0; + for (int i=0; i timerComparator = new Comparator() { + @Override + public int compare(TimerEntry lhs, TimerEntry rhs) { + long lhsTime = lhs.mTime; + long rhsTime = rhs.mTime; + if (lhsTime < rhsTime) { + return 1; + } + if (lhsTime > rhsTime) { + return -1; + } + return 0; + } + }; + + if (reqUid < 0) { + Map kernelWakelocks = getKernelWakelockStats(); + if (kernelWakelocks.size() > 0) { + final ArrayList ktimers = new ArrayList(); + for (Map.Entry ent : kernelWakelocks.entrySet()) { + BatteryStats.Timer timer = ent.getValue(); + long totalTimeMillis = computeWakeLock(timer, rawRealtime, which); + if (totalTimeMillis > 0) { + ktimers.add(new TimerEntry(ent.getKey(), 0, timer, totalTimeMillis)); + } + } + if (ktimers.size() > 0) { + Collections.sort(ktimers, timerComparator); + pw.print(prefix); pw.println(" All kernel wake locks:"); + for (int i=0; i 0) { + Collections.sort(timers, timerComparator); + pw.print(prefix); pw.println(" All partial wake locks:"); + for (int i=0; i wakeupReasons = getWakeupReasonStats(); + if (wakeupReasons.size() > 0) { + pw.print(prefix); pw.println(" All wakeup reasons:"); + final ArrayList reasons = new ArrayList(); + for (Map.Entry ent : wakeupReasons.entrySet()) { + Timer timer = ent.getValue(); + reasons.add(new TimerEntry(ent.getKey(), 0, timer, + timer.getCountLocked(which))); + } + Collections.sort(reasons, timerComparator); + for (int i=0; i= 0 && uid != reqUid && uid != Process.SYSTEM_UID) { + continue; + } + + Uid u = uidStats.valueAt(iu); + + pw.print(prefix); + pw.print(" "); + UserHandle.formatUid(pw, uid); + pw.println(":"); + boolean uidActivity = false; + + long mobileRxBytes = u.getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which); + long mobileTxBytes = u.getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which); + long wifiRxBytes = u.getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which); + long wifiTxBytes = u.getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which); + long mobileRxPackets = u.getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which); + long mobileTxPackets = u.getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which); + long uidMobileActiveTime = u.getMobileRadioActiveTime(which); + int uidMobileActiveCount = u.getMobileRadioActiveCount(which); + long wifiRxPackets = u.getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which); + long wifiTxPackets = u.getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which); + long fullWifiLockOnTime = u.getFullWifiLockTime(rawRealtime, which); + long wifiScanTime = u.getWifiScanTime(rawRealtime, which); + long uidWifiRunningTime = u.getWifiRunningTime(rawRealtime, which); + + if (mobileRxBytes > 0 || mobileTxBytes > 0 + || mobileRxPackets > 0 || mobileTxPackets > 0) { + pw.print(prefix); pw.print(" Mobile network: "); + pw.print(formatBytesLocked(mobileRxBytes)); pw.print(" received, "); + pw.print(formatBytesLocked(mobileTxBytes)); + pw.print(" sent (packets "); pw.print(mobileRxPackets); + pw.print(" received, "); pw.print(mobileTxPackets); pw.println(" sent)"); + } + if (uidMobileActiveTime > 0 || uidMobileActiveCount > 0) { + sb.setLength(0); + sb.append(prefix); sb.append(" Mobile radio active: "); + formatTimeMs(sb, uidMobileActiveTime / 1000); + sb.append("("); + sb.append(formatRatioLocked(uidMobileActiveTime, mobileActiveTime)); + sb.append(") "); sb.append(uidMobileActiveCount); sb.append("x"); + long packets = mobileRxPackets + mobileTxPackets; + if (packets == 0) { + packets = 1; + } + sb.append(" @ "); + sb.append(BatteryStatsHelper.makemAh(uidMobileActiveTime / 1000 / (double)packets)); + sb.append(" mspp"); + pw.println(sb.toString()); + } + + if (wifiRxBytes > 0 || wifiTxBytes > 0 || wifiRxPackets > 0 || wifiTxPackets > 0) { + pw.print(prefix); pw.print(" Wi-Fi network: "); + pw.print(formatBytesLocked(wifiRxBytes)); pw.print(" received, "); + pw.print(formatBytesLocked(wifiTxBytes)); + pw.print(" sent (packets "); pw.print(wifiRxPackets); + pw.print(" received, "); pw.print(wifiTxPackets); pw.println(" sent)"); + } + + if (fullWifiLockOnTime != 0 || wifiScanTime != 0 + || uidWifiRunningTime != 0) { + sb.setLength(0); + sb.append(prefix); sb.append(" Wifi Running: "); + formatTimeMs(sb, uidWifiRunningTime / 1000); + sb.append("("); sb.append(formatRatioLocked(uidWifiRunningTime, + whichBatteryRealtime)); sb.append(")\n"); + sb.append(prefix); sb.append(" Full Wifi Lock: "); + formatTimeMs(sb, fullWifiLockOnTime / 1000); + sb.append("("); sb.append(formatRatioLocked(fullWifiLockOnTime, + whichBatteryRealtime)); sb.append(")\n"); + sb.append(prefix); sb.append(" Wifi Scan: "); + formatTimeMs(sb, wifiScanTime / 1000); + sb.append("("); sb.append(formatRatioLocked(wifiScanTime, + whichBatteryRealtime)); sb.append(")"); + pw.println(sb.toString()); + } + + if (u.hasUserActivity()) { + boolean hasData = false; + for (int i=0; i wakelocks = u.getWakelockStats(); + if (wakelocks.size() > 0) { + long totalFull = 0, totalPartial = 0, totalWindow = 0; + int count = 0; + for (Map.Entry ent : wakelocks.entrySet()) { + Uid.Wakelock wl = ent.getValue(); + String linePrefix = ": "; + sb.setLength(0); + sb.append(prefix); + sb.append(" Wake lock "); + sb.append(ent.getKey()); + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_FULL), rawRealtime, + "full", which, linePrefix); + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_PARTIAL), rawRealtime, + "partial", which, linePrefix); + linePrefix = printWakeLock(sb, wl.getWakeTime(WAKE_TYPE_WINDOW), rawRealtime, + "window", which, linePrefix); + if (true || !linePrefix.equals(": ")) { + sb.append(" realtime"); + // Only print out wake locks that were held + pw.println(sb.toString()); + uidActivity = true; + count++; + } + totalFull += computeWakeLock(wl.getWakeTime(WAKE_TYPE_FULL), + rawRealtime, which); + totalPartial += computeWakeLock(wl.getWakeTime(WAKE_TYPE_PARTIAL), + rawRealtime, which); + totalWindow += computeWakeLock(wl.getWakeTime(WAKE_TYPE_WINDOW), + rawRealtime, which); + } + if (count > 1) { + if (totalFull != 0 || totalPartial != 0 || totalWindow != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" TOTAL wake: "); + boolean needComma = false; + if (totalFull != 0) { + needComma = true; + formatTimeMs(sb, totalFull); + sb.append("full"); + } + if (totalPartial != 0) { + if (needComma) { + sb.append(", "); + } + needComma = true; + formatTimeMs(sb, totalPartial); + sb.append("partial"); + } + if (totalWindow != 0) { + if (needComma) { + sb.append(", "); + } + needComma = true; + formatTimeMs(sb, totalWindow); + sb.append("window"); + } + sb.append(" realtime"); + pw.println(sb.toString()); + } + } + } + + Map syncs = u.getSyncStats(); + if (syncs.size() > 0) { + for (Map.Entry ent : syncs.entrySet()) { + Timer timer = ent.getValue(); + // Convert from microseconds to milliseconds with rounding + long totalTime = (timer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; + int count = timer.getCountLocked(which); + sb.setLength(0); + sb.append(prefix); + sb.append(" Sync "); + sb.append(ent.getKey()); + sb.append(": "); + if (totalTime != 0) { + formatTimeMs(sb, totalTime); + sb.append("realtime ("); + sb.append(count); + sb.append(" times)"); + } else { + sb.append("(not used)"); + } + pw.println(sb.toString()); + uidActivity = true; + } + } + + Map jobs = u.getJobStats(); + if (jobs.size() > 0) { + for (Map.Entry ent : jobs.entrySet()) { + Timer timer = ent.getValue(); + // Convert from microseconds to milliseconds with rounding + long totalTime = (timer.getTotalTimeLocked(rawRealtime, which) + 500) / 1000; + int count = timer.getCountLocked(which); + sb.setLength(0); + sb.append(prefix); + sb.append(" Job "); + sb.append(ent.getKey()); + sb.append(": "); + if (totalTime != 0) { + formatTimeMs(sb, totalTime); + sb.append("realtime ("); + sb.append(count); + sb.append(" times)"); + } else { + sb.append("(not used)"); + } + pw.println(sb.toString()); + uidActivity = true; + } + } + + SparseArray sensors = u.getSensorStats(); + int NSE = sensors.size(); + for (int ise=0; ise 0) { + totalStateTime += time; + sb.setLength(0); + sb.append(prefix); + sb.append(" "); + sb.append(Uid.PROCESS_STATE_NAMES[ips]); + sb.append(" for: "); + formatTimeMs(sb, (totalStateTime + 500) / 1000); + pw.println(sb.toString()); + uidActivity = true; + } + } + + Map processStats = u.getProcessStats(); + if (processStats.size() > 0) { + for (Map.Entry ent + : processStats.entrySet()) { + Uid.Proc ps = ent.getValue(); + long userTime; + long systemTime; + long foregroundTime; + int starts; + int numExcessive; + + userTime = ps.getUserTime(which); + systemTime = ps.getSystemTime(which); + foregroundTime = ps.getForegroundTime(which); + starts = ps.getStarts(which); + final int numCrashes = ps.getNumCrashes(which); + final int numAnrs = ps.getNumAnrs(which); + numExcessive = which == STATS_SINCE_CHARGED + ? ps.countExcessivePowers() : 0; + + if (userTime != 0 || systemTime != 0 || foregroundTime != 0 || starts != 0 + || numExcessive != 0 || numCrashes != 0 || numAnrs != 0) { + sb.setLength(0); + sb.append(prefix); sb.append(" Proc "); + sb.append(ent.getKey()); sb.append(":\n"); + sb.append(prefix); sb.append(" CPU: "); + formatTime(sb, userTime); sb.append("usr + "); + formatTime(sb, systemTime); sb.append("krn ; "); + formatTime(sb, foregroundTime); sb.append("fg"); + if (starts != 0 || numCrashes != 0 || numAnrs != 0) { + sb.append("\n"); sb.append(prefix); sb.append(" "); + boolean hasOne = false; + if (starts != 0) { + hasOne = true; + sb.append(starts); sb.append(" starts"); + } + if (numCrashes != 0) { + if (hasOne) { + sb.append(", "); + } + hasOne = true; + sb.append(numCrashes); sb.append(" crashes"); + } + if (numAnrs != 0) { + if (hasOne) { + sb.append(", "); + } + sb.append(numAnrs); sb.append(" anrs"); + } + } + pw.println(sb.toString()); + for (int e=0; e packageStats = u.getPackageStats(); + if (packageStats.size() > 0) { + for (Map.Entry ent + : packageStats.entrySet()) { + pw.print(prefix); pw.print(" Apk "); pw.print(ent.getKey()); pw.println(":"); + boolean apkActivity = false; + Uid.Pkg ps = ent.getValue(); + int wakeups = ps.getWakeups(which); + if (wakeups != 0) { + pw.print(prefix); pw.print(" "); + pw.print(wakeups); pw.println(" wakeup alarms"); + apkActivity = true; + } + Map serviceStats = ps.getServiceStats(); + if (serviceStats.size() > 0) { + for (Map.Entry sent + : serviceStats.entrySet()) { + BatteryStats.Uid.Pkg.Serv ss = sent.getValue(); + long startTime = ss.getStartTime(batteryUptime, which); + int starts = ss.getStarts(which); + int launches = ss.getLaunches(which); + if (startTime != 0 || starts != 0 || launches != 0) { + sb.setLength(0); + sb.append(prefix); sb.append(" Service "); + sb.append(sent.getKey()); sb.append(":\n"); + sb.append(prefix); sb.append(" Created for: "); + formatTimeMs(sb, startTime / 1000); + sb.append("uptime\n"); + sb.append(prefix); sb.append(" Starts: "); + sb.append(starts); + sb.append(", launches: "); sb.append(launches); + pw.println(sb.toString()); + apkActivity = true; + } + } + } + if (!apkActivity) { + pw.print(prefix); pw.println(" (nothing executed)"); + } + uidActivity = true; + } + } + if (!uidActivity) { + pw.print(prefix); pw.println(" (nothing executed)"); + } + } + } + + static void printBitDescriptions(PrintWriter pw, int oldval, int newval, HistoryTag wakelockTag, + BitDescription[] descriptions, boolean longNames) { + int diff = oldval ^ newval; + if (diff == 0) return; + boolean didWake = false; + for (int i=0; i= 0 && idx < eventNames.length) { + pw.print(eventNames[idx]); + } else { + pw.print(checkin ? "Ev" : "event"); + pw.print(idx); + } + pw.print("="); + if (checkin) { + pw.print(rec.eventTag.poolIdx); + } else { + UserHandle.formatUid(pw, rec.eventTag.uid); + pw.print(":\""); + pw.print(rec.eventTag.string); + pw.print("\""); + } + } + pw.println(); + oldState = rec.states; + oldState2 = rec.states2; + } + } + } + + private void printSizeValue(PrintWriter pw, long size) { + float result = size; + String suffix = ""; + if (result >= 10*1024) { + suffix = "KB"; + result = result / 1024; + } + if (result >= 10*1024) { + suffix = "MB"; + result = result / 1024; + } + if (result >= 10*1024) { + suffix = "GB"; + result = result / 1024; + } + if (result >= 10*1024) { + suffix = "TB"; + result = result / 1024; + } + if (result >= 10*1024) { + suffix = "PB"; + result = result / 1024; + } + pw.print((int)result); + pw.print(suffix); + } + + private static boolean dumpTimeEstimate(PrintWriter pw, String label, long[] steps, + int count, long modesOfInterest, long modeValues) { + if (count <= 0) { + return false; + } + long total = 0; + int numOfInterest = 0; + for (int i=0; i> STEP_LEVEL_INITIAL_MODE_SHIFT; + long modMode = (steps[i] & STEP_LEVEL_MODIFIED_MODE_MASK) + >> STEP_LEVEL_MODIFIED_MODE_SHIFT; + // If the modes of interest didn't change during this step period... + if ((modMode&modesOfInterest) == 0) { + // And the mode values during this period match those we are measuring... + if ((initMode&modesOfInterest) == modeValues) { + // Then this can be used to estimate the total time! + numOfInterest++; + total += steps[i] & STEP_LEVEL_TIME_MASK; + } + } + } + if (numOfInterest <= 0) { + return false; + } + + // The estimated time is the average time we spend in each level, multipled + // by 100 -- the total number of battery levels + long estimatedTime = (total / numOfInterest) * 100; + + pw.print(label); + StringBuilder sb = new StringBuilder(64); + formatTimeMs(sb, estimatedTime); + pw.print(sb); + pw.println(); + + return true; + } + + private static boolean dumpDurationSteps(PrintWriter pw, String header, long[] steps, + int count, boolean checkin) { + if (count <= 0) { + return false; + } + if (!checkin) { + pw.println(header); + } + String[] lineArgs = new String[4]; + for (int i=0; i> STEP_LEVEL_LEVEL_SHIFT); + long initMode = (steps[i] & STEP_LEVEL_INITIAL_MODE_MASK) + >> STEP_LEVEL_INITIAL_MODE_SHIFT; + long modMode = (steps[i] & STEP_LEVEL_MODIFIED_MODE_MASK) + >> STEP_LEVEL_MODIFIED_MODE_SHIFT; + if (checkin) { + lineArgs[0] = Long.toString(duration); + lineArgs[1] = Integer.toString(level); + if ((modMode&STEP_LEVEL_MODE_SCREEN_STATE) == 0) { + switch ((int)(initMode&STEP_LEVEL_MODE_SCREEN_STATE) + 1) { + case Display.STATE_OFF: lineArgs[2] = "s-"; break; + case Display.STATE_ON: lineArgs[2] = "s+"; break; + case Display.STATE_DOZE: lineArgs[2] = "sd"; break; + case Display.STATE_DOZE_SUSPEND: lineArgs[2] = "sds"; break; + default: lineArgs[1] = "?"; break; + } + } else { + lineArgs[2] = ""; + } + if ((modMode&STEP_LEVEL_MODE_POWER_SAVE) == 0) { + lineArgs[3] = (initMode&STEP_LEVEL_MODE_POWER_SAVE) != 0 ? "p+" : "p-"; + } else { + lineArgs[3] = ""; + } + dumpLine(pw, 0 /* uid */, "i" /* category */, header, (Object[])lineArgs); + } else { + pw.print(" #"); pw.print(i); pw.print(": "); + TimeUtils.formatDuration(duration, pw); + pw.print(" to "); pw.print(level); + boolean haveModes = false; + if ((modMode&STEP_LEVEL_MODE_SCREEN_STATE) == 0) { + pw.print(" ("); + switch ((int)(initMode&STEP_LEVEL_MODE_SCREEN_STATE) + 1) { + case Display.STATE_OFF: pw.print("screen-off"); break; + case Display.STATE_ON: pw.print("screen-on"); break; + case Display.STATE_DOZE: pw.print("screen-doze"); break; + case Display.STATE_DOZE_SUSPEND: pw.print("screen-doze-suspend"); break; + default: lineArgs[1] = "screen-?"; break; + } + haveModes = true; + } + if ((modMode&STEP_LEVEL_MODE_POWER_SAVE) == 0) { + pw.print(haveModes ? ", " : " ("); + pw.print((initMode&STEP_LEVEL_MODE_POWER_SAVE) != 0 + ? "power-save-on" : "power-save-off"); + haveModes = true; + } + if (haveModes) { + pw.print(")"); + } + pw.println(); + } + } + return true; + } + + public static final int DUMP_UNPLUGGED_ONLY = 1<<0; + public static final int DUMP_CHARGED_ONLY = 1<<1; + public static final int DUMP_HISTORY_ONLY = 1<<2; + public static final int DUMP_INCLUDE_HISTORY = 1<<3; + public static final int DUMP_VERBOSE = 1<<4; + public static final int DUMP_DEVICE_WIFI_ONLY = 1<<5; + + private void dumpHistoryLocked(PrintWriter pw, int flags, long histStart, boolean checkin) { + final HistoryPrinter hprinter = new HistoryPrinter(); + final HistoryItem rec = new HistoryItem(); + long lastTime = -1; + long baseTime = -1; + boolean printed = false; + HistoryEventTracker tracker = null; + while (getNextHistoryLocked(rec)) { + lastTime = rec.time; + if (baseTime < 0) { + baseTime = lastTime; + } + if (rec.time >= histStart) { + if (histStart >= 0 && !printed) { + if (rec.cmd == HistoryItem.CMD_CURRENT_TIME + || rec.cmd == HistoryItem.CMD_RESET + || rec.cmd == HistoryItem.CMD_START + || rec.cmd == HistoryItem.CMD_SHUTDOWN) { + printed = true; + hprinter.printNextItem(pw, rec, baseTime, checkin, + (flags&DUMP_VERBOSE) != 0); + rec.cmd = HistoryItem.CMD_UPDATE; + } else if (rec.currentTime != 0) { + printed = true; + byte cmd = rec.cmd; + rec.cmd = HistoryItem.CMD_CURRENT_TIME; + hprinter.printNextItem(pw, rec, baseTime, checkin, + (flags&DUMP_VERBOSE) != 0); + rec.cmd = cmd; + } + if (tracker != null) { + if (rec.cmd != HistoryItem.CMD_UPDATE) { + hprinter.printNextItem(pw, rec, baseTime, checkin, + (flags&DUMP_VERBOSE) != 0); + rec.cmd = HistoryItem.CMD_UPDATE; + } + int oldEventCode = rec.eventCode; + HistoryTag oldEventTag = rec.eventTag; + rec.eventTag = new HistoryTag(); + for (int i=0; i active + = tracker.getStateForEvent(i); + if (active == null) { + continue; + } + for (HashMap.Entry ent + : active.entrySet()) { + SparseIntArray uids = ent.getValue(); + for (int j=0; j= 0) { + commitCurrentHistoryBatchLocked(); + pw.print(checkin ? "NEXT: " : " NEXT: "); pw.println(lastTime+1); + } + } + + /** + * Dumps a human-readable summary of the battery statistics to the given PrintWriter. + * + * @param pw a Printer to receive the dump output. + */ + @SuppressWarnings("unused") + public void dumpLocked(Context context, PrintWriter pw, int flags, int reqUid, long histStart) { + prepareForDumpLocked(); + + final boolean filtering = + (flags&(DUMP_HISTORY_ONLY|DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) != 0; + + if ((flags&DUMP_HISTORY_ONLY) != 0 || !filtering) { + final long historyTotalSize = getHistoryTotalSize(); + final long historyUsedSize = getHistoryUsedSize(); + if (startIteratingHistoryLocked()) { + try { + pw.print("Battery History ("); + pw.print((100*historyUsedSize)/historyTotalSize); + pw.print("% used, "); + printSizeValue(pw, historyUsedSize); + pw.print(" used of "); + printSizeValue(pw, historyTotalSize); + pw.print(", "); + pw.print(getHistoryStringPoolSize()); + pw.print(" strings using "); + printSizeValue(pw, getHistoryStringPoolBytes()); + pw.println("):"); + dumpHistoryLocked(pw, flags, histStart, false); + pw.println(); + } finally { + finishIteratingHistoryLocked(); + } + } + + if (startIteratingOldHistoryLocked()) { + try { + final HistoryItem rec = new HistoryItem(); + pw.println("Old battery History:"); + HistoryPrinter hprinter = new HistoryPrinter(); + long baseTime = -1; + while (getNextOldHistoryLocked(rec)) { + if (baseTime < 0) { + baseTime = rec.time; + } + hprinter.printNextItem(pw, rec, baseTime, false, (flags&DUMP_VERBOSE) != 0); + } + pw.println(); + } finally { + finishIteratingOldHistoryLocked(); + } + } + } + + if (filtering && (flags&(DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) == 0) { + return; + } + + if (!filtering) { + SparseArray uidStats = getUidStats(); + final int NU = uidStats.size(); + boolean didPid = false; + long nowRealtime = SystemClock.elapsedRealtime(); + for (int i=0; i pids = uid.getPidStats(); + if (pids != null) { + for (int j=0; j 0 + ? (nowRealtime - pid.mWakeStartMs) : 0); + pw.print(" PID "); pw.print(pids.keyAt(j)); + pw.print(" wake time: "); + TimeUtils.formatDuration(time, pw); + pw.println(""); + } + } + } + if (didPid) { + pw.println(); + } + } + + if (!filtering || (flags&DUMP_CHARGED_ONLY) != 0) { + if (dumpDurationSteps(pw, "Discharge step durations:", getDischargeStepDurationsArray(), + getNumDischargeStepDurations(), false)) { + long timeRemaining = computeBatteryTimeRemaining(SystemClock.elapsedRealtime()); + if (timeRemaining >= 0) { + pw.print(" Estimated discharge time remaining: "); + TimeUtils.formatDuration(timeRemaining / 1000, pw); + pw.println(); + } + dumpTimeEstimate(pw, " Estimated screen off time: ", + getDischargeStepDurationsArray(), getNumDischargeStepDurations(), + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_OFF-1)); + dumpTimeEstimate(pw, " Estimated screen off power save time: ", + getDischargeStepDurationsArray(), getNumDischargeStepDurations(), + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_OFF-1)|STEP_LEVEL_MODE_POWER_SAVE); + dumpTimeEstimate(pw, " Estimated screen on time: ", + getDischargeStepDurationsArray(), getNumDischargeStepDurations(), + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_ON-1)); + dumpTimeEstimate(pw, " Estimated screen on power save time: ", + getDischargeStepDurationsArray(), getNumDischargeStepDurations(), + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_ON-1)|STEP_LEVEL_MODE_POWER_SAVE); + dumpTimeEstimate(pw, " Estimated screen doze time: ", + getDischargeStepDurationsArray(), getNumDischargeStepDurations(), + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_DOZE-1)); + dumpTimeEstimate(pw, " Estimated screen doze power save time: ", + getDischargeStepDurationsArray(), getNumDischargeStepDurations(), + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_DOZE-1)|STEP_LEVEL_MODE_POWER_SAVE); + dumpTimeEstimate(pw, " Estimated screen doze suspend time: ", + getDischargeStepDurationsArray(), getNumDischargeStepDurations(), + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_DOZE_SUSPEND-1)); + dumpTimeEstimate(pw, " Estimated screen doze suspend power save time: ", + getDischargeStepDurationsArray(), getNumDischargeStepDurations(), + STEP_LEVEL_MODE_SCREEN_STATE|STEP_LEVEL_MODE_POWER_SAVE, + (Display.STATE_DOZE_SUSPEND-1)|STEP_LEVEL_MODE_POWER_SAVE); + pw.println(); + } + if (dumpDurationSteps(pw, "Charge step durations:", getChargeStepDurationsArray(), + getNumChargeStepDurations(), false)) { + long timeRemaining = computeChargeTimeRemaining(SystemClock.elapsedRealtime()); + if (timeRemaining >= 0) { + pw.print(" Estimated charge time remaining: "); + TimeUtils.formatDuration(timeRemaining / 1000, pw); + pw.println(); + } + pw.println(); + } + pw.println("Statistics since last charge:"); + pw.println(" System starts: " + getStartCount() + + ", currently on battery: " + getIsOnBattery()); + dumpLocked(context, pw, "", STATS_SINCE_CHARGED, reqUid, + (flags&DUMP_DEVICE_WIFI_ONLY) != 0); + pw.println(); + } + if (!filtering || (flags&DUMP_UNPLUGGED_ONLY) != 0) { + pw.println("Statistics since last unplugged:"); + dumpLocked(context, pw, "", STATS_SINCE_UNPLUGGED, reqUid, + (flags&DUMP_DEVICE_WIFI_ONLY) != 0); + } + } + + @SuppressWarnings("unused") + public void dumpCheckinLocked(Context context, PrintWriter pw, + List apps, int flags, long histStart) { + prepareForDumpLocked(); + + dumpLine(pw, 0 /* uid */, "i" /* category */, VERSION_DATA, + "12", getParcelVersion(), getStartPlatformVersion(), getEndPlatformVersion()); + + long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); + + final boolean filtering = + (flags&(DUMP_HISTORY_ONLY|DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) != 0; + + if ((flags&DUMP_INCLUDE_HISTORY) != 0 || (flags&DUMP_HISTORY_ONLY) != 0) { + if (startIteratingHistoryLocked()) { + try { + for (int i=0; i> uids = new SparseArray>(); + for (int i=0; i pkgs = uids.get(ai.uid); + if (pkgs == null) { + pkgs = new ArrayList(); + uids.put(ai.uid, pkgs); + } + pkgs.add(ai.packageName); + } + SparseArray uidStats = getUidStats(); + final int NU = uidStats.size(); + String[] lineArgs = new String[2]; + for (int i=0; i pkgs = uids.get(uid); + if (pkgs != null) { + for (int j=0; j= 0) { + lineArgs[0] = Long.toString(timeRemaining); + dumpLine(pw, 0 /* uid */, "i" /* category */, DISCHARGE_TIME_REMAIN_DATA, + (Object[])lineArgs); + } + dumpDurationSteps(pw, CHARGE_STEP_DATA, getChargeStepDurationsArray(), + getNumChargeStepDurations(), true); + timeRemaining = computeChargeTimeRemaining(SystemClock.elapsedRealtime()); + if (timeRemaining >= 0) { + lineArgs[0] = Long.toString(timeRemaining); + dumpLine(pw, 0 /* uid */, "i" /* category */, CHARGE_TIME_REMAIN_DATA, + (Object[])lineArgs); + } + dumpCheckinLocked(context, pw, STATS_SINCE_CHARGED, -1, + (flags&DUMP_DEVICE_WIFI_ONLY) != 0); + } + if (!filtering || (flags&DUMP_UNPLUGGED_ONLY) != 0) { + dumpCheckinLocked(context, pw, STATS_SINCE_UNPLUGGED, -1, + (flags&DUMP_DEVICE_WIFI_ONLY) != 0); + } + } +} diff --git a/src/main/java/android/os/Binder.java b/src/main/java/android/os/Binder.java new file mode 100644 index 0000000..b4a4624 --- /dev/null +++ b/src/main/java/android/os/Binder.java @@ -0,0 +1,562 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.util.Log; +import android.util.Slog; +import com.android.internal.util.FastPrintWriter; + +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.ref.WeakReference; +import java.lang.reflect.Modifier; + +/** + * Base class for a remotable object, the core part of a lightweight + * remote procedure call mechanism defined by {@link IBinder}. + * This class is an implementation of IBinder that provides + * standard local implementation of such an object. + * + *

Most developers will not implement this class directly, instead using the + * aidl tool to describe the desired + * interface, having it generate the appropriate Binder subclass. You can, + * however, derive directly from Binder to implement your own custom RPC + * protocol or simply instantiate a raw Binder object directly to use as a + * token that can be shared across processes. + * + *

This class is just a basic IPC primitive; it has no impact on an application's + * lifecycle, and is valid only as long as the process that created it continues to run. + * To use this correctly, you must be doing so within the context of a top-level + * application component (a {@link android.app.Service}, {@link android.app.Activity}, + * or {@link android.content.ContentProvider}) that lets the system know your process + * should remain running.

+ * + *

You must keep in mind the situations in which your process + * could go away, and thus require that you later re-create a new Binder and re-attach + * it when the process starts again. For example, if you are using this within an + * {@link android.app.Activity}, your activity's process may be killed any time the + * activity is not started; if the activity is later re-created you will need to + * create a new Binder and hand it back to the correct place again; you need to be + * aware that your process may be started for another reason (for example to receive + * a broadcast) that will not involve re-creating the activity and thus run its code + * to create a new Binder.

+ * + * @see IBinder + */ +public class Binder implements IBinder { + /* + * Set this flag to true to detect anonymous, local or member classes + * that extend this Binder class and that are not static. These kind + * of classes can potentially create leaks. + */ + private static final boolean FIND_POTENTIAL_LEAKS = false; + private static final boolean CHECK_PARCEL_SIZE = false; + static final String TAG = "Binder"; + + /** + * Control whether dump() calls are allowed. + */ + private static String sDumpDisabled = null; + + /* mObject is used by native code, do not remove or rename */ + private long mObject; + private IInterface mOwner; + private String mDescriptor; + + /** + * Return the ID of the process that sent you the current transaction + * that is being processed. This pid can be used with higher-level + * system services to determine its identity and check permissions. + * If the current thread is not currently executing an incoming transaction, + * then its own pid is returned. + */ + public static final native int getCallingPid(); + + /** + * Return the Linux uid assigned to the process that sent you the + * current transaction that is being processed. This uid can be used with + * higher-level system services to determine its identity and check + * permissions. If the current thread is not currently executing an + * incoming transaction, then its own uid is returned. + */ + public static final native int getCallingUid(); + + /** + * Return the UserHandle assigned to the process that sent you the + * current transaction that is being processed. This is the user + * of the caller. It is distinct from {@link #getCallingUid()} in that a + * particular user will have multiple distinct apps running under it each + * with their own uid. If the current thread is not currently executing an + * incoming transaction, then its own UserHandle is returned. + */ + public static final UserHandle getCallingUserHandle() { + return new UserHandle(UserHandle.getUserId(getCallingUid())); + } + + /** + * Reset the identity of the incoming IPC on the current thread. This can + * be useful if, while handling an incoming call, you will be calling + * on interfaces of other objects that may be local to your process and + * need to do permission checks on the calls coming into them (so they + * will check the permission of your own local process, and not whatever + * process originally called you). + * + * @return Returns an opaque token that can be used to restore the + * original calling identity by passing it to + * {@link #restoreCallingIdentity(long)}. + * + * @see #getCallingPid() + * @see #getCallingUid() + * @see #restoreCallingIdentity(long) + */ + public static final native long clearCallingIdentity(); + + /** + * Restore the identity of the incoming IPC on the current thread + * back to a previously identity that was returned by {@link + * #clearCallingIdentity}. + * + * @param token The opaque token that was previously returned by + * {@link #clearCallingIdentity}. + * + * @see #clearCallingIdentity + */ + public static final native void restoreCallingIdentity(long token); + + /** + * Sets the native thread-local StrictMode policy mask. + * + *

The StrictMode settings are kept in two places: a Java-level + * threadlocal for libcore/Dalvik, and a native threadlocal (set + * here) for propagation via Binder calls. This is a little + * unfortunate, but necessary to break otherwise more unfortunate + * dependencies either of Dalvik on Android, or Android + * native-only code on Dalvik. + * + * @see StrictMode + * @hide + */ + public static final native void setThreadStrictModePolicy(int policyMask); + + /** + * Gets the current native thread-local StrictMode policy mask. + * + * @see #setThreadStrictModePolicy + * @hide + */ + public static final native int getThreadStrictModePolicy(); + + /** + * Flush any Binder commands pending in the current thread to the kernel + * driver. This can be + * useful to call before performing an operation that may block for a long + * time, to ensure that any pending object references have been released + * in order to prevent the process from holding on to objects longer than + * it needs to. + */ + public static final native void flushPendingCommands(); + + /** + * Add the calling thread to the IPC thread pool. This function does + * not return until the current process is exiting. + */ + public static final native void joinThreadPool(); + + /** + * Returns true if the specified interface is a proxy. + * @hide + */ + public static final boolean isProxy(IInterface iface) { + return iface.asBinder() != iface; + } + + /** + * Default constructor initializes the object. + */ + public Binder() { + init(); + + if (FIND_POTENTIAL_LEAKS) { + final Class klass = getClass(); + if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && + (klass.getModifiers() & Modifier.STATIC) == 0) { + Log.w(TAG, "The following Binder class should be static or leaks might occur: " + + klass.getCanonicalName()); + } + } + } + + /** + * Convenience method for associating a specific interface with the Binder. + * After calling, queryLocalInterface() will be implemented for you + * to return the given owner IInterface when the corresponding + * descriptor is requested. + */ + public void attachInterface(IInterface owner, String descriptor) { + mOwner = owner; + mDescriptor = descriptor; + } + + /** + * Default implementation returns an empty interface name. + */ + public String getInterfaceDescriptor() { + return mDescriptor; + } + + /** + * Default implementation always returns true -- if you got here, + * the object is alive. + */ + public boolean pingBinder() { + return true; + } + + /** + * {@inheritDoc} + * + * Note that if you're calling on a local binder, this always returns true + * because your process is alive if you're calling it. + */ + public boolean isBinderAlive() { + return true; + } + + /** + * Use information supplied to attachInterface() to return the + * associated IInterface if it matches the requested + * descriptor. + */ + public IInterface queryLocalInterface(String descriptor) { + if (mDescriptor.equals(descriptor)) { + return mOwner; + } + return null; + } + + /** + * Control disabling of dump calls in this process. This is used by the system + * process watchdog to disable incoming dump calls while it has detecting the system + * is hung and is reporting that back to the activity controller. This is to + * prevent the controller from getting hung up on bug reports at this point. + * @hide + * + * @param msg The message to show instead of the dump; if null, dumps are + * re-enabled. + */ + public static void setDumpDisabled(String msg) { + synchronized (Binder.class) { + sDumpDisabled = msg; + } + } + + /** + * Default implementation is a stub that returns false. You will want + * to override this to do the appropriate unmarshalling of transactions. + * + *

If you want to call this, call transact(). + */ + protected boolean onTransact(int code, Parcel data, Parcel reply, + int flags) throws RemoteException { + if (code == INTERFACE_TRANSACTION) { + reply.writeString(getInterfaceDescriptor()); + return true; + } else if (code == DUMP_TRANSACTION) { + ParcelFileDescriptor fd = data.readFileDescriptor(); + String[] args = data.readStringArray(); + if (fd != null) { + try { + dump(fd.getFileDescriptor(), args); + } finally { + try { + fd.close(); + } catch (IOException e) { + // swallowed, not propagated back to the caller + } + } + } + // Write the StrictMode header. + if (reply != null) { + reply.writeNoException(); + } else { + StrictMode.clearGatheredViolations(); + } + return true; + } + return false; + } + + /** + * Implemented to call the more convenient version + * {@link #dump(FileDescriptor, PrintWriter, String[])}. + */ + public void dump(FileDescriptor fd, String[] args) { + FileOutputStream fout = new FileOutputStream(fd); + PrintWriter pw = new FastPrintWriter(fout); + try { + final String disabled; + synchronized (Binder.class) { + disabled = sDumpDisabled; + } + if (disabled == null) { + try { + dump(fd, pw, args); + } catch (SecurityException e) { + pw.println("Security exception: " + e.getMessage()); + throw e; + } catch (Throwable e) { + // Unlike usual calls, in this case if an exception gets thrown + // back to us we want to print it back in to the dump data, since + // that is where the caller expects all interesting information to + // go. + pw.println(); + pw.println("Exception occurred while dumping:"); + e.printStackTrace(pw); + } + } else { + pw.println(sDumpDisabled); + } + } finally { + pw.flush(); + } + } + + /** + * Like {@link #dump(FileDescriptor, String[])}, but ensures the target + * executes asynchronously. + */ + public void dumpAsync(final FileDescriptor fd, final String[] args) { + final FileOutputStream fout = new FileOutputStream(fd); + final PrintWriter pw = new FastPrintWriter(fout); + Thread thr = new Thread("Binder.dumpAsync") { + public void run() { + try { + dump(fd, pw, args); + } finally { + pw.flush(); + } + } + }; + thr.start(); + } + + /** + * Print the object's state into the given stream. + * + * @param fd The raw file descriptor that the dump is being sent to. + * @param fout The file to which you should dump your state. This will be + * closed for you after you return. + * @param args additional arguments to the dump request. + */ + protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + } + + /** + * Default implementation rewinds the parcels and calls onTransact. On + * the remote side, transact calls into the binder to do the IPC. + */ + public final boolean transact(int code, Parcel data, Parcel reply, + int flags) throws RemoteException { + if (false) Log.v("Binder", "Transact: " + code + " to " + this); + if (data != null) { + data.setDataPosition(0); + } + boolean r = onTransact(code, data, reply, flags); + if (reply != null) { + reply.setDataPosition(0); + } + return r; + } + + /** + * Local implementation is a no-op. + */ + public void linkToDeath(DeathRecipient recipient, int flags) { + } + + /** + * Local implementation is a no-op. + */ + public boolean unlinkToDeath(DeathRecipient recipient, int flags) { + return true; + } + + protected void finalize() throws Throwable { + try { + destroy(); + } finally { + super.finalize(); + } + } + + static void checkParcel(IBinder obj, int code, Parcel parcel, String msg) { + if (CHECK_PARCEL_SIZE && parcel.dataSize() >= 800*1024) { + // Trying to send > 800k, this is way too much + StringBuilder sb = new StringBuilder(); + sb.append(msg); + sb.append(": on "); + sb.append(obj); + sb.append(" calling "); + sb.append(code); + sb.append(" size "); + sb.append(parcel.dataSize()); + sb.append(" (data: "); + parcel.setDataPosition(0); + sb.append(parcel.readInt()); + sb.append(", "); + sb.append(parcel.readInt()); + sb.append(", "); + sb.append(parcel.readInt()); + sb.append(")"); + Slog.wtfStack(TAG, sb.toString()); + } + } + + private native final void init(); + private native final void destroy(); + + // Entry point from android_util_Binder.cpp's onTransact + private boolean execTransact(int code, long dataObj, long replyObj, + int flags) { + Parcel data = Parcel.obtain(dataObj); + Parcel reply = Parcel.obtain(replyObj); + // theoretically, we should call transact, which will call onTransact, + // but all that does is rewind it, and we just got these from an IPC, + // so we'll just call it directly. + boolean res; + // Log any exceptions as warnings, don't silently suppress them. + // If the call was FLAG_ONEWAY then these exceptions disappear into the ether. + try { + res = onTransact(code, data, reply, flags); + } catch (RemoteException e) { + if ((flags & FLAG_ONEWAY) != 0) { + Log.w(TAG, "Binder call failed.", e); + } else { + reply.setDataPosition(0); + reply.writeException(e); + } + res = true; + } catch (RuntimeException e) { + if ((flags & FLAG_ONEWAY) != 0) { + Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e); + } else { + reply.setDataPosition(0); + reply.writeException(e); + } + res = true; + } catch (OutOfMemoryError e) { + // Unconditionally log this, since this is generally unrecoverable. + Log.e(TAG, "Caught an OutOfMemoryError from the binder stub implementation.", e); + RuntimeException re = new RuntimeException("Out of memory", e); + reply.setDataPosition(0); + reply.writeException(re); + res = true; + } + checkParcel(this, code, reply, "Unreasonably large binder reply buffer"); + reply.recycle(); + data.recycle(); + + // Just in case -- we are done with the IPC, so there should be no more strict + // mode violations that have gathered for this thread. Either they have been + // parceled and are now in transport off to the caller, or we are returning back + // to the main transaction loop to wait for another incoming transaction. Either + // way, strict mode begone! + StrictMode.clearGatheredViolations(); + + return res; + } +} + +final class BinderProxy implements IBinder { + public native boolean pingBinder(); + public native boolean isBinderAlive(); + + public IInterface queryLocalInterface(String descriptor) { + return null; + } + + public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { + Binder.checkParcel(this, code, data, "Unreasonably large binder buffer"); + return transactNative(code, data, reply, flags); + } + + public native String getInterfaceDescriptor() throws RemoteException; + public native boolean transactNative(int code, Parcel data, Parcel reply, + int flags) throws RemoteException; + public native void linkToDeath(DeathRecipient recipient, int flags) + throws RemoteException; + public native boolean unlinkToDeath(DeathRecipient recipient, int flags); + + public void dump(FileDescriptor fd, String[] args) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeFileDescriptor(fd); + data.writeStringArray(args); + try { + transact(DUMP_TRANSACTION, data, reply, 0); + reply.readException(); + } finally { + data.recycle(); + reply.recycle(); + } + } + + public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeFileDescriptor(fd); + data.writeStringArray(args); + try { + transact(DUMP_TRANSACTION, data, reply, FLAG_ONEWAY); + } finally { + data.recycle(); + reply.recycle(); + } + } + + BinderProxy() { + mSelf = new WeakReference(this); + } + + @Override + protected void finalize() throws Throwable { + try { + destroy(); + } finally { + super.finalize(); + } + } + + private native final void destroy(); + + private static final void sendDeathNotice(DeathRecipient recipient) { + if (false) Log.v("JavaBinder", "sendDeathNotice to " + recipient); + try { + recipient.binderDied(); + } + catch (RuntimeException exc) { + Log.w("BinderNative", "Uncaught exception from death notification", + exc); + } + } + + final private WeakReference mSelf; + private long mObject; + private long mOrgue; +} diff --git a/src/main/java/android/os/BinderThreadPriorityService.java b/src/main/java/android/os/BinderThreadPriorityService.java new file mode 100644 index 0000000..47a4483 --- /dev/null +++ b/src/main/java/android/os/BinderThreadPriorityService.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 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 android.os; + +import android.app.Service; +import android.content.Intent; +import android.text.TextUtils; +import android.util.Log; + +/** + * Service used by {@link BinderThreadPriorityTest} to verify + * the conveyance of thread priorities over Binder. + */ +public class BinderThreadPriorityService extends Service { + private static final String TAG = "BinderThreadPriorityService"; + + private final IBinderThreadPriorityService.Stub mBinder = + new IBinderThreadPriorityService.Stub() { + public int getThreadPriority() { + return Process.getThreadPriority(Process.myTid()); + } + + public String getThreadSchedulerGroup() { + return BinderThreadPriorityTest.getSchedulerGroup(); + } + + public void callBack(IBinderThreadPriorityService recurse) { + try { + recurse.callBack(this); + } catch (RemoteException e) { + Log.e(TAG, "Binder callback failed", e); + } + } + + public void setPriorityAndCallBack(int priority, IBinderThreadPriorityService recurse) { + Process.setThreadPriority(priority); + try { + recurse.callBack(this); + } catch (RemoteException e) { + Log.e(TAG, "Binder callback failed", e); + } + } + }; + + public IBinder onBind(Intent intent) { + return mBinder; + } +} diff --git a/src/main/java/android/os/BinderThreadPriorityTest.java b/src/main/java/android/os/BinderThreadPriorityTest.java new file mode 100644 index 0000000..7a4980a --- /dev/null +++ b/src/main/java/android/os/BinderThreadPriorityTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2010 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 android.os; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.MediumTest; +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +/** + * Test whether Binder calls inherit thread priorities correctly. + */ +public class BinderThreadPriorityTest extends AndroidTestCase { + private static final String TAG = "BinderThreadPriorityTest"; + private IBinderThreadPriorityService mService; + private int mSavedPriority; + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName name, IBinder service) { + synchronized (BinderThreadPriorityTest.this) { + mService = IBinderThreadPriorityService.Stub.asInterface(service); + BinderThreadPriorityTest.this.notifyAll(); + } + } + + public void onServiceDisconnected(ComponentName name) { + mService = null; + } + }; + + private static class ServiceStub extends IBinderThreadPriorityService.Stub { + public int getThreadPriority() { fail(); return -999; } + public String getThreadSchedulerGroup() { fail(); return null; } + public void setPriorityAndCallBack(int p, IBinderThreadPriorityService cb) { fail(); } + public void callBack(IBinderThreadPriorityService cb) { fail(); } + private static void fail() { throw new RuntimeException("unimplemented"); } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + getContext().bindService( + new Intent(getContext(), BinderThreadPriorityService.class), + mConnection, Context.BIND_AUTO_CREATE); + + synchronized (this) { + if (mService == null) { + try { + wait(30000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + assertNotNull("Gave up waiting for BinderThreadPriorityService", mService); + } + } + + mSavedPriority = Process.getThreadPriority(Process.myTid()); + Process.setThreadPriority(mSavedPriority); // To realign priority & cgroup, if needed + assertEquals(expectedSchedulerGroup(mSavedPriority), getSchedulerGroup()); + Log.i(TAG, "Saved priority: " + mSavedPriority); + } + + @Override + protected void tearDown() throws Exception { + // HACK -- see bug 2665914 -- setThreadPriority() doesn't always set the + // scheduler group reliably unless we start out with background priority. + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + Process.setThreadPriority(mSavedPriority); + assertEquals(mSavedPriority, Process.getThreadPriority(Process.myTid())); + assertEquals(expectedSchedulerGroup(mSavedPriority), getSchedulerGroup()); + + getContext().unbindService(mConnection); + super.tearDown(); + } + + public static String getSchedulerGroup() { + String fn = "/proc/" + Process.myPid() + "/task/" + Process.myTid() + "/cgroup"; + try { + String cgroup = FileUtils.readTextFile(new File(fn), 1024, null); + for (String line : cgroup.split("\n")) { + String fields[] = line.trim().split(":"); + if (fields.length == 3 && fields[1].equals("cpu")) return fields[2]; + } + } catch (IOException e) { + Log.e(TAG, "Can't read: " + fn, e); + } + return null; // Unknown + } + + public static String expectedSchedulerGroup(int prio) { + return prio < Process.THREAD_PRIORITY_BACKGROUND ? "/" : "/bg_non_interactive"; + } + + public void testPassPriorityToService() throws Exception { + for (int prio = 19; prio >= -20; prio--) { + Process.setThreadPriority(prio); + + // Local + assertEquals(prio, Process.getThreadPriority(Process.myTid())); + assertEquals(expectedSchedulerGroup(prio), getSchedulerGroup()); + + // Remote + assertEquals(prio, mService.getThreadPriority()); + assertEquals(expectedSchedulerGroup(prio), mService.getThreadSchedulerGroup()); + } + } + + public void testCallBackFromServiceWithPriority() throws Exception { + for (int prio = -20; prio <= 19; prio++) { + final int expected = prio; + mService.setPriorityAndCallBack(prio, new ServiceStub() { + public void callBack(IBinderThreadPriorityService cb) { + assertEquals(expected, Process.getThreadPriority(Process.myTid())); + assertEquals(expectedSchedulerGroup(expected), getSchedulerGroup()); + } + }); + + assertEquals(mSavedPriority, Process.getThreadPriority(Process.myTid())); + + // BROKEN -- see bug 2665954 -- scheduler group doesn't get reset + // properly after a back-call with a different priority. + // assertEquals(expectedSchedulerGroup(mSavedPriority), getSchedulerGroup()); + } + } +} diff --git a/src/main/java/android/os/BrightnessLimit.java b/src/main/java/android/os/BrightnessLimit.java new file mode 100644 index 0000000..f4a5e09 --- /dev/null +++ b/src/main/java/android/os/BrightnessLimit.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2008 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 android.os; + +import android.os.IPowerManager; + +import android.app.Activity; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.Settings; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; + +import com.android.frameworks.coretests.R; + +/** + * Tries to set the brightness to 0. Should be silently thwarted by the framework. + */ +public class BrightnessLimit extends Activity implements OnClickListener { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.brightness_limit); + + Button b = (Button) findViewById(R.id.go); + b.setOnClickListener(this); + } + + public void onClick(View v) { + IPowerManager power = IPowerManager.Stub.asInterface( + ServiceManager.getService("power")); + if (power != null) { + try { + power.setTemporaryScreenBrightnessSettingOverride(0); + } catch (RemoteException darn) { + + } + } + Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, 0); + } +} + diff --git a/src/main/java/android/os/Broadcaster.java b/src/main/java/android/os/Broadcaster.java new file mode 100644 index 0000000..70dcdd8 --- /dev/null +++ b/src/main/java/android/os/Broadcaster.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2006 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 android.os; + +/** @hide */ +public class Broadcaster +{ + public Broadcaster() + { + } + + /** + * Sign up for notifications about something. + * + * When this broadcaster pushes a message with senderWhat in the what field, + * target will be sent a copy of that message with targetWhat in the what field. + */ + public void request(int senderWhat, Handler target, int targetWhat) + { + synchronized (this) { + Registration r = null; + if (mReg == null) { + r = new Registration(); + r.senderWhat = senderWhat; + r.targets = new Handler[1]; + r.targetWhats = new int[1]; + r.targets[0] = target; + r.targetWhats[0] = targetWhat; + mReg = r; + r.next = r; + r.prev = r; + } else { + // find its place in the map + Registration start = mReg; + r = start; + do { + if (r.senderWhat >= senderWhat) { + break; + } + r = r.next; + } while (r != start); + int n; + if (r.senderWhat != senderWhat) { + // we didn't find a senderWhat match, but r is right + // after where it goes + Registration reg = new Registration(); + reg.senderWhat = senderWhat; + reg.targets = new Handler[1]; + reg.targetWhats = new int[1]; + reg.next = r; + reg.prev = r.prev; + r.prev.next = reg; + r.prev = reg; + + if (r == mReg && r.senderWhat > reg.senderWhat) { + mReg = reg; + } + + r = reg; + n = 0; + } else { + n = r.targets.length; + Handler[] oldTargets = r.targets; + int[] oldWhats = r.targetWhats; + // check for duplicates, and don't do it if we are dup. + for (int i=0; i= senderWhat) { + break; + } + r = r.next; + } while (r != start); + + if (r.senderWhat == senderWhat) { + Handler[] targets = r.targets; + int[] whats = r.targetWhats; + int oldLen = targets.length; + for (int i=0; i 0) { + System.arraycopy(targets, 0, r.targets, 0, i); + System.arraycopy(whats, 0, r.targetWhats, 0, i); + } + + int remainingLen = oldLen-i-1; + if (remainingLen != 0) { + System.arraycopy(targets, i+1, r.targets, i, + remainingLen); + System.arraycopy(whats, i+1, r.targetWhats, i, + remainingLen); + } + break; + } + } + } + } + } + + /** + * For debugging purposes, print the registrations to System.out + */ + public void dumpRegistrations() + { + synchronized (this) { + Registration start = mReg; + System.out.println("Broadcaster " + this + " {"); + if (start != null) { + Registration r = start; + do { + System.out.println(" senderWhat=" + r.senderWhat); + int n = r.targets.length; + for (int i=0; i= senderWhat) { + break; + } + r = r.next; + } while (r != start); + if (r.senderWhat == senderWhat) { + Handler[] targets = r.targets; + int[] whats = r.targetWhats; + int n = targets.length; + for (int i=0; i= N) { + failure(); + } else { + if (msg.getTarget() == mHandlers[index]) { + mSuccess[index] = true; + } + } + boolean winner = true; + for (int i = 0; i < N; i++) { + if (!mSuccess[i]) { + winner = false; + } + } + if (winner) { + success(); + } + } + } + + @MediumTest + public void test2() throws Exception { + /* + * 2 handlers request the same message, with different translations + */ + HandlerTester tester = new Tests2and3(2); + tester.doTest(1000); + } + + @MediumTest + public void test3() throws Exception { + /* + * 1000 handlers request the same message, with different translations + */ + HandlerTester tester = new Tests2and3(10); + tester.doTest(1000); + } + + @MediumTest + public void test4() throws Exception { + /* + * Two handlers request different messages, with translations, sending + * only one. The other one should never get sent. + */ + HandlerTester tester = new HandlerTester() { + Handler h1; + Handler h2; + + public void go() { + Broadcaster b = new Broadcaster(); + h1 = new H(); + h2 = new H(); + + b.request(MESSAGE_A, h1, MESSAGE_C); + b.request(MESSAGE_B, h2, MESSAGE_D); + + Message msg = new Message(); + msg.what = MESSAGE_A; + + b.broadcast(msg); + } + + public void handleMessage(Message msg) { + if (msg.what == MESSAGE_C && msg.getTarget() == h1) { + success(); + } else { + failure(); + } + } + }; + tester.doTest(1000); + } + + @MediumTest + public void test5() throws Exception { + /* + * Two handlers request different messages, with translations, sending + * only one. The other one should never get sent. + */ + HandlerTester tester = new HandlerTester() { + Handler h1; + Handler h2; + + public void go() { + Broadcaster b = new Broadcaster(); + h1 = new H(); + h2 = new H(); + + b.request(MESSAGE_A, h1, MESSAGE_C); + b.request(MESSAGE_B, h2, MESSAGE_D); + + Message msg = new Message(); + msg.what = MESSAGE_B; + + b.broadcast(msg); + } + + public void handleMessage(Message msg) { + if (msg.what == MESSAGE_D && msg.getTarget() == h2) { + success(); + } else { + failure(); + } + } + }; + tester.doTest(1000); + } + + @MediumTest + public void test6() throws Exception { + /* + * Two handlers request same message. Cancel the request for the + * 2nd handler, make sure the first still works. + */ + HandlerTester tester = new HandlerTester() { + Handler h1; + Handler h2; + + public void go() { + Broadcaster b = new Broadcaster(); + h1 = new H(); + h2 = new H(); + + b.request(MESSAGE_A, h1, MESSAGE_C); + b.request(MESSAGE_A, h2, MESSAGE_D); + b.cancelRequest(MESSAGE_A, h2, MESSAGE_D); + + Message msg = new Message(); + msg.what = MESSAGE_A; + + b.broadcast(msg); + } + + public void handleMessage(Message msg) { + if (msg.what == MESSAGE_C && msg.getTarget() == h1) { + success(); + } else { + failure(); + } + } + }; + tester.doTest(1000); + } +} diff --git a/src/main/java/android/os/Build.java b/src/main/java/android/os/Build.java new file mode 100644 index 0000000..4b0cef6 --- /dev/null +++ b/src/main/java/android/os/Build.java @@ -0,0 +1,712 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.text.TextUtils; +import android.util.Slog; + +import com.android.internal.telephony.TelephonyProperties; + +import dalvik.system.VMRuntime; + +import java.util.Objects; + +/** + * Information about the current build, extracted from system properties. + */ +public class Build { + private static final String TAG = "Build"; + + /** Value used for when a build property is unknown. */ + public static final String UNKNOWN = "unknown"; + + /** Either a changelist number, or a label like "M4-rc20". */ + public static final String ID = getString("ro.build.id"); + + /** A build ID string meant for displaying to the user */ + public static final String DISPLAY = getString("ro.build.display.id"); + + /** The name of the overall product. */ + public static final String PRODUCT = getString("ro.product.name"); + + /** The name of the industrial design. */ + public static final String DEVICE = getString("ro.product.device"); + + /** The name of the underlying board, like "goldfish". */ + public static final String BOARD = getString("ro.product.board"); + + /** + * The name of the instruction set (CPU type + ABI convention) of native code. + * + * @deprecated Use {@link #SUPPORTED_ABIS} instead. + */ + @Deprecated + public static final String CPU_ABI; + + /** + * The name of the second instruction set (CPU type + ABI convention) of native code. + * + * @deprecated Use {@link #SUPPORTED_ABIS} instead. + */ + @Deprecated + public static final String CPU_ABI2; + + /** The manufacturer of the product/hardware. */ + public static final String MANUFACTURER = getString("ro.product.manufacturer"); + + /** The consumer-visible brand with which the product/hardware will be associated, if any. */ + public static final String BRAND = getString("ro.product.brand"); + + /** The end-user-visible name for the end product. */ + public static final String MODEL = getString("ro.product.model"); + + /** The system bootloader version number. */ + public static final String BOOTLOADER = getString("ro.bootloader"); + + /** + * The radio firmware version number. + * + * @deprecated The radio firmware version is frequently not + * available when this class is initialized, leading to a blank or + * "unknown" value for this string. Use + * {@link #getRadioVersion} instead. + */ + @Deprecated + public static final String RADIO = getString(TelephonyProperties.PROPERTY_BASEBAND_VERSION); + + /** The name of the hardware (from the kernel command line or /proc). */ + public static final String HARDWARE = getString("ro.hardware"); + + /** A hardware serial number, if available. Alphanumeric only, case-insensitive. */ + public static final String SERIAL = getString("ro.serialno"); + + /** + * An ordered list of ABIs supported by this device. The most preferred ABI is the first + * element in the list. + * + * See {@link #SUPPORTED_32_BIT_ABIS} and {@link #SUPPORTED_64_BIT_ABIS}. + */ + public static final String[] SUPPORTED_ABIS = getStringList("ro.product.cpu.abilist", ","); + + /** + * An ordered list of 32 bit ABIs supported by this device. The most preferred ABI + * is the first element in the list. + * + * See {@link #SUPPORTED_ABIS} and {@link #SUPPORTED_64_BIT_ABIS}. + */ + public static final String[] SUPPORTED_32_BIT_ABIS = + getStringList("ro.product.cpu.abilist32", ","); + + /** + * An ordered list of 64 bit ABIs supported by this device. The most preferred ABI + * is the first element in the list. + * + * See {@link #SUPPORTED_ABIS} and {@link #SUPPORTED_32_BIT_ABIS}. + */ + public static final String[] SUPPORTED_64_BIT_ABIS = + getStringList("ro.product.cpu.abilist64", ","); + + + static { + /* + * Adjusts CPU_ABI and CPU_ABI2 depending on whether or not a given process is 64 bit. + * 32 bit processes will always see 32 bit ABIs in these fields for backward + * compatibility. + */ + final String[] abiList; + if (VMRuntime.getRuntime().is64Bit()) { + abiList = SUPPORTED_64_BIT_ABIS; + } else { + abiList = SUPPORTED_32_BIT_ABIS; + } + + CPU_ABI = abiList[0]; + if (abiList.length > 1) { + CPU_ABI2 = abiList[1]; + } else { + CPU_ABI2 = ""; + } + } + + /** Various version strings. */ + public static class VERSION { + /** + * The internal value used by the underlying source control to + * represent this build. E.g., a perforce changelist number + * or a git hash. + */ + public static final String INCREMENTAL = getString("ro.build.version.incremental"); + + /** + * The user-visible version string. E.g., "1.0" or "3.4b5". + */ + public static final String RELEASE = getString("ro.build.version.release"); + + /** + * The user-visible SDK version of the framework in its raw String + * representation; use {@link #SDK_INT} instead. + * + * @deprecated Use {@link #SDK_INT} to easily get this as an integer. + */ + @Deprecated + public static final String SDK = getString("ro.build.version.sdk"); + + /** + * The user-visible SDK version of the framework; its possible + * values are defined in {@link Build.VERSION_CODES}. + */ + public static final int SDK_INT = SystemProperties.getInt( + "ro.build.version.sdk", 0); + + /** + * The current development codename, or the string "REL" if this is + * a release build. + */ + public static final String CODENAME = getString("ro.build.version.codename"); + + private static final String[] ALL_CODENAMES + = getStringList("ro.build.version.all_codenames", ","); + + /** + * @hide + */ + public static final String[] ACTIVE_CODENAMES = "REL".equals(ALL_CODENAMES[0]) + ? new String[0] : ALL_CODENAMES; + + /** + * The SDK version to use when accessing resources. + * Use the current SDK version code. For every active development codename + * we are operating under, we bump the assumed resource platform version by 1. + * @hide + */ + public static final int RESOURCES_SDK_INT = SDK_INT + ACTIVE_CODENAMES.length; + } + + /** + * Enumeration of the currently known SDK version codes. These are the + * values that can be found in {@link VERSION#SDK}. Version numbers + * increment monotonically with each official platform release. + */ + public static class VERSION_CODES { + /** + * Magic version number for a current development build, which has + * not yet turned into an official release. + */ + public static final int CUR_DEVELOPMENT = 10000; + + /** + * October 2008: The original, first, version of Android. Yay! + */ + public static final int BASE = 1; + + /** + * February 2009: First Android update, officially called 1.1. + */ + public static final int BASE_1_1 = 2; + + /** + * May 2009: Android 1.5. + */ + public static final int CUPCAKE = 3; + + /** + * September 2009: Android 1.6. + * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • They must explicitly request the + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permission to be + * able to modify the contents of the SD card. (Apps targeting + * earlier versions will always request the permission.) + *
  • They must explicitly request the + * {@link android.Manifest.permission#READ_PHONE_STATE} permission to be + * able to be able to retrieve phone state info. (Apps targeting + * earlier versions will always request the permission.) + *
  • They are assumed to support different screen densities and + * sizes. (Apps targeting earlier versions are assumed to only support + * medium density normal size screens unless otherwise indicated). + * They can still explicitly specify screen support either way with the + * supports-screens manifest tag. + *
  • {@link android.widget.TabHost} will use the new dark tab + * background design. + *
+ */ + public static final int DONUT = 4; + + /** + * November 2009: Android 2.0 + * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • The {@link android.app.Service#onStartCommand + * Service.onStartCommand} function will return the new + * {@link android.app.Service#START_STICKY} behavior instead of the + * old compatibility {@link android.app.Service#START_STICKY_COMPATIBILITY}. + *
  • The {@link android.app.Activity} class will now execute back + * key presses on the key up instead of key down, to be able to detect + * canceled presses from virtual keys. + *
  • The {@link android.widget.TabWidget} class will use a new color scheme + * for tabs. In the new scheme, the foreground tab has a medium gray background + * the background tabs have a dark gray background. + *
+ */ + public static final int ECLAIR = 5; + + /** + * December 2009: Android 2.0.1 + */ + public static final int ECLAIR_0_1 = 6; + + /** + * January 2010: Android 2.1 + */ + public static final int ECLAIR_MR1 = 7; + + /** + * June 2010: Android 2.2 + */ + public static final int FROYO = 8; + + /** + * November 2010: Android 2.3 + * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • The application's notification icons will be shown on the new + * dark status bar background, so must be visible in this situation. + *
+ */ + public static final int GINGERBREAD = 9; + + /** + * February 2011: Android 2.3.3. + */ + public static final int GINGERBREAD_MR1 = 10; + + /** + * February 2011: Android 3.0. + * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • The default theme for applications is now dark holographic: + * {@link android.R.style#Theme_Holo}. + *
  • On large screen devices that do not have a physical menu + * button, the soft (compatibility) menu is disabled. + *
  • The activity lifecycle has changed slightly as per + * {@link android.app.Activity}. + *
  • An application will crash if it does not call through + * to the super implementation of its + * {@link android.app.Activity#onPause Activity.onPause()} method. + *
  • When an application requires a permission to access one of + * its components (activity, receiver, service, provider), this + * permission is no longer enforced when the application wants to + * access its own component. This means it can require a permission + * on a component that it does not itself hold and still access that + * component. + *
  • {@link android.content.Context#getSharedPreferences + * Context.getSharedPreferences()} will not automatically reload + * the preferences if they have changed on storage, unless + * {@link android.content.Context#MODE_MULTI_PROCESS} is used. + *
  • {@link android.view.ViewGroup#setMotionEventSplittingEnabled} + * will default to true. + *
  • {@link android.view.WindowManager.LayoutParams#FLAG_SPLIT_TOUCH} + * is enabled by default on windows. + *
  • {@link android.widget.PopupWindow#isSplitTouchEnabled() + * PopupWindow.isSplitTouchEnabled()} will return true by default. + *
  • {@link android.widget.GridView} and {@link android.widget.ListView} + * will use {@link android.view.View#setActivated View.setActivated} + * for selected items if they do not implement {@link android.widget.Checkable}. + *
  • {@link android.widget.Scroller} will be constructed with + * "flywheel" behavior enabled by default. + *
+ */ + public static final int HONEYCOMB = 11; + + /** + * May 2011: Android 3.1. + */ + public static final int HONEYCOMB_MR1 = 12; + + /** + * June 2011: Android 3.2. + * + *

Update to Honeycomb MR1 to support 7 inch tablets, improve + * screen compatibility mode, etc.

+ * + *

As of this version, applications that don't say whether they + * support XLARGE screens will be assumed to do so only if they target + * {@link #HONEYCOMB} or later; it had been {@link #GINGERBREAD} or + * later. Applications that don't support a screen size at least as + * large as the current screen will provide the user with a UI to + * switch them in to screen size compatibility mode.

+ * + *

This version introduces new screen size resource qualifiers + * based on the screen size in dp: see + * {@link android.content.res.Configuration#screenWidthDp}, + * {@link android.content.res.Configuration#screenHeightDp}, and + * {@link android.content.res.Configuration#smallestScreenWidthDp}. + * Supplying these in <supports-screens> as per + * {@link android.content.pm.ApplicationInfo#requiresSmallestWidthDp}, + * {@link android.content.pm.ApplicationInfo#compatibleWidthLimitDp}, and + * {@link android.content.pm.ApplicationInfo#largestWidthLimitDp} is + * preferred over the older screen size buckets and for older devices + * the appropriate buckets will be inferred from them.

+ * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • New {@link android.content.pm.PackageManager#FEATURE_SCREEN_PORTRAIT} + * and {@link android.content.pm.PackageManager#FEATURE_SCREEN_LANDSCAPE} + * features were introduced in this release. Applications that target + * previous platform versions are assumed to require both portrait and + * landscape support in the device; when targeting Honeycomb MR1 or + * greater the application is responsible for specifying any specific + * orientation it requires.

    + *
  • {@link android.os.AsyncTask} will use the serial executor + * by default when calling {@link android.os.AsyncTask#execute}.

    + *
  • {@link android.content.pm.ActivityInfo#configChanges + * ActivityInfo.configChanges} will have the + * {@link android.content.pm.ActivityInfo#CONFIG_SCREEN_SIZE} and + * {@link android.content.pm.ActivityInfo#CONFIG_SMALLEST_SCREEN_SIZE} + * bits set; these need to be cleared for older applications because + * some developers have done absolute comparisons against this value + * instead of correctly masking the bits they are interested in. + *

+ */ + public static final int HONEYCOMB_MR2 = 13; + + /** + * October 2011: Android 4.0. + * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • For devices without a dedicated menu key, the software compatibility + * menu key will not be shown even on phones. By targeting Ice Cream Sandwich + * or later, your UI must always have its own menu UI affordance if needed, + * on both tablets and phones. The ActionBar will take care of this for you. + *
  • 2d drawing hardware acceleration is now turned on by default. + * You can use + * {@link android.R.attr#hardwareAccelerated android:hardwareAccelerated} + * to turn it off if needed, although this is strongly discouraged since + * it will result in poor performance on larger screen devices. + *
  • The default theme for applications is now the "device default" theme: + * {@link android.R.style#Theme_DeviceDefault}. This may be the + * holo dark theme or a different dark theme defined by the specific device. + * The {@link android.R.style#Theme_Holo} family must not be modified + * for a device to be considered compatible. Applications that explicitly + * request a theme from the Holo family will be guaranteed that these themes + * will not change character within the same platform version. Applications + * that wish to blend in with the device should use a theme from the + * {@link android.R.style#Theme_DeviceDefault} family. + *
  • Managed cursors can now throw an exception if you directly close + * the cursor yourself without stopping the management of it; previously failures + * would be silently ignored. + *
  • The fadingEdge attribute on views will be ignored (fading edges is no + * longer a standard part of the UI). A new requiresFadingEdge attribute allows + * applications to still force fading edges on for special cases. + *
  • {@link android.content.Context#bindService Context.bindService()} + * will not automatically add in {@link android.content.Context#BIND_WAIVE_PRIORITY}. + *
  • App Widgets will have standard padding automatically added around + * them, rather than relying on the padding being baked into the widget itself. + *
  • An exception will be thrown if you try to change the type of a + * window after it has been added to the window manager. Previously this + * would result in random incorrect behavior. + *
  • {@link android.view.animation.AnimationSet} will parse out + * the duration, fillBefore, fillAfter, repeatMode, and startOffset + * XML attributes that are defined. + *
  • {@link android.app.ActionBar#setHomeButtonEnabled + * ActionBar.setHomeButtonEnabled()} is false by default. + *
+ */ + public static final int ICE_CREAM_SANDWICH = 14; + + /** + * December 2011: Android 4.0.3. + */ + public static final int ICE_CREAM_SANDWICH_MR1 = 15; + + /** + * June 2012: Android 4.1. + * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • You must explicitly request the {@link android.Manifest.permission#READ_CALL_LOG} + * and/or {@link android.Manifest.permission#WRITE_CALL_LOG} permissions; + * access to the call log is no longer implicitly provided through + * {@link android.Manifest.permission#READ_CONTACTS} and + * {@link android.Manifest.permission#WRITE_CONTACTS}. + *
  • {@link android.widget.RemoteViews} will throw an exception if + * setting an onClick handler for views being generated by a + * {@link android.widget.RemoteViewsService} for a collection container; + * previously this just resulted in a warning log message. + *
  • New {@link android.app.ActionBar} policy for embedded tabs: + * embedded tabs are now always stacked in the action bar when in portrait + * mode, regardless of the size of the screen. + *
  • {@link android.webkit.WebSettings#setAllowFileAccessFromFileURLs(boolean) + * WebSettings.setAllowFileAccessFromFileURLs} and + * {@link android.webkit.WebSettings#setAllowUniversalAccessFromFileURLs(boolean) + * WebSettings.setAllowUniversalAccessFromFileURLs} default to false. + *
  • Calls to {@link android.content.pm.PackageManager#setComponentEnabledSetting + * PackageManager.setComponentEnabledSetting} will now throw an + * IllegalArgumentException if the given component class name does not + * exist in the application's manifest. + *
  • {@link android.nfc.NfcAdapter#setNdefPushMessage + * NfcAdapter.setNdefPushMessage}, + * {@link android.nfc.NfcAdapter#setNdefPushMessageCallback + * NfcAdapter.setNdefPushMessageCallback} and + * {@link android.nfc.NfcAdapter#setOnNdefPushCompleteCallback + * NfcAdapter.setOnNdefPushCompleteCallback} will throw + * IllegalStateException if called after the Activity has been destroyed. + *
  • Accessibility services must require the new + * {@link android.Manifest.permission#BIND_ACCESSIBILITY_SERVICE} permission or + * they will not be available for use. + *
  • {@link android.accessibilityservice.AccessibilityServiceInfo#FLAG_INCLUDE_NOT_IMPORTANT_VIEWS + * AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS} must be set + * for unimportant views to be included in queries. + *
+ */ + public static final int JELLY_BEAN = 16; + + /** + * November 2012: Android 4.2, Moar jelly beans! + * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • Content Providers: The default value of {@code android:exported} is now + * {@code false}. See + * + * the android:exported section in the provider documentation for more details.
  • + *
  • {@link android.view.View#getLayoutDirection() View.getLayoutDirection()} + * can return different values than {@link android.view.View#LAYOUT_DIRECTION_LTR} + * based on the locale etc. + *
  • {@link android.webkit.WebView#addJavascriptInterface(Object, String) + * WebView.addJavascriptInterface} requires explicit annotations on methods + * for them to be accessible from Javascript. + *
+ */ + public static final int JELLY_BEAN_MR1 = 17; + + /** + * July 2013: Android 4.3, the revenge of the beans. + */ + public static final int JELLY_BEAN_MR2 = 18; + + /** + * October 2013: Android 4.4, KitKat, another tasty treat. + * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • The default result of {android.preference.PreferenceActivity#isValidFragment + * PreferenceActivity.isValueFragment} becomes false instead of true.
  • + *
  • In {@link android.webkit.WebView}, apps targeting earlier versions will have + * JS URLs evaluated directly and any result of the evaluation will not replace + * the current page content. Apps targetting KITKAT or later that load a JS URL will + * have the result of that URL replace the content of the current page
  • + *
  • {@link android.app.AlarmManager#set AlarmManager.set} becomes interpreted as + * an inexact value, to give the system more flexibility in scheduling alarms.
  • + *
  • {@link android.content.Context#getSharedPreferences(String, int) + * Context.getSharedPreferences} no longer allows a null name.
  • + *
  • {@link android.widget.RelativeLayout} changes to compute wrapped content + * margins correctly.
  • + *
  • {@link android.app.ActionBar}'s window content overlay is allowed to be + * drawn.
  • + *
  • The {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} + * permission is now always enforced.
  • + *
  • Access to package-specific external storage directories belonging + * to the calling app no longer requires the + * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} or + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} + * permissions.
  • + *
+ */ + public static final int KITKAT = 19; + + /** + * Android 4.4W: KitKat for watches, snacks on the run. + * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • {@link android.app.AlertDialog} might not have a default background if the theme does + * not specify one.
  • + *
+ */ + public static final int KITKAT_WATCH = 20; + + /** + * Temporary until we completely switch to {@link #LOLLIPOP}. + * @hide + */ + public static final int L = 21; + + /** + * Lollipop. A flat one with beautiful shadows. But still tasty. + * + *

Applications targeting this or a later release will get these + * new changes in behavior:

+ *
    + *
  • {@link android.content.Context#bindService Context.bindService} now + * requires an explicit Intent, and will throw an exception if given an implicit + * Intent.
  • + *
  • {@link android.app.Notification.Builder Notification.Builder} will + * not have the colors of their various notification elements adjusted to better + * match the new material design look.
  • + *
  • {@link android.os.Message} will validate that a message is not currently + * in use when it is recycled.
  • + *
  • Hardware accelerated drawing in windows will be enabled automatically + * in most places.
  • + *
  • {@link android.widget.Spinner} throws an exception if attaching an + * adapter with more than one item type.
  • + *
  • If the app is a launcher, the launcher will be available to the user + * even when they are using corporate profiles (which requires that the app + * use {@link android.content.pm.LauncherApps} to correctly populate its + * apps UI).
  • + *
  • Calling {@link android.app.Service#stopForeground Service.stopForeground} + * with removeNotification false will modify the still posted notification so that + * it is no longer forced to be ongoing.
  • + *
  • A {@link android.service.dreams.DreamService} must require the + * {@link android.Manifest.permission#BIND_DREAM_SERVICE} permission to be usable.
  • + *
+ */ + public static final int LOLLIPOP = 21; + + /** + * Lollipop with an extra sugar coating on the outside! + */ + public static final int LOLLIPOP_MR1 = 22; + } + + /** The type of build, like "user" or "eng". */ + public static final String TYPE = getString("ro.build.type"); + + /** Comma-separated tags describing the build, like "unsigned,debug". */ + public static final String TAGS = getString("ro.build.tags"); + + /** A string that uniquely identifies this build. Do not attempt to parse this value. */ + public static final String FINGERPRINT = deriveFingerprint(); + + /** + * Some devices split the fingerprint components between multiple + * partitions, so we might derive the fingerprint at runtime. + */ + private static String deriveFingerprint() { + String finger = SystemProperties.get("ro.build.fingerprint"); + if (TextUtils.isEmpty(finger)) { + finger = getString("ro.product.brand") + '/' + + getString("ro.product.name") + '/' + + getString("ro.product.device") + ':' + + getString("ro.build.version.release") + '/' + + getString("ro.build.id") + '/' + + getString("ro.build.version.incremental") + ':' + + getString("ro.build.type") + '/' + + getString("ro.build.tags"); + } + return finger; + } + + /** + * Ensure that raw fingerprint system property is defined. If it was derived + * dynamically by {@link #deriveFingerprint()} this is where we push the + * derived value into the property service. + * + * @hide + */ + public static void ensureFingerprintProperty() { + if (TextUtils.isEmpty(SystemProperties.get("ro.build.fingerprint"))) { + try { + SystemProperties.set("ro.build.fingerprint", FINGERPRINT); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Failed to set fingerprint property", e); + } + } + } + + /** + * Check that device fingerprint is defined and that it matches across + * various partitions. + * + * @hide + */ + public static boolean isFingerprintConsistent() { + final String system = SystemProperties.get("ro.build.fingerprint"); + final String vendor = SystemProperties.get("ro.vendor.build.fingerprint"); + + if (TextUtils.isEmpty(system)) { + Slog.e(TAG, "Required ro.build.fingerprint is empty!"); + return false; + } + + if (!TextUtils.isEmpty(vendor)) { + if (!Objects.equals(system, vendor)) { + Slog.e(TAG, "Mismatched fingerprints; system reported " + system + + " but vendor reported " + vendor); + return false; + } + } + + return true; + } + + // The following properties only make sense for internal engineering builds. + public static final long TIME = getLong("ro.build.date.utc") * 1000; + public static final String USER = getString("ro.build.user"); + public static final String HOST = getString("ro.build.host"); + + /** + * Returns true if we are running a debug build such as "user-debug" or "eng". + * @hide + */ + public static final boolean IS_DEBUGGABLE = + SystemProperties.getInt("ro.debuggable", 0) == 1; + + /** + * Returns the version string for the radio firmware. May return + * null (if, for instance, the radio is not currently on). + */ + public static String getRadioVersion() { + return SystemProperties.get(TelephonyProperties.PROPERTY_BASEBAND_VERSION, null); + } + + private static String getString(String property) { + return SystemProperties.get(property, UNKNOWN); + } + + private static String[] getStringList(String property, String separator) { + String value = SystemProperties.get(property); + if (value.isEmpty()) { + return new String[0]; + } else { + return value.split(separator); + } + } + + private static long getLong(String property) { + try { + return Long.parseLong(SystemProperties.get(property)); + } catch (NumberFormatException e) { + return -1; + } + } +} diff --git a/src/main/java/android/os/BuildTest.java b/src/main/java/android/os/BuildTest.java new file mode 100644 index 0000000..3758627 --- /dev/null +++ b/src/main/java/android/os/BuildTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2008 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 android.os; + +import android.os.Build; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; +import junit.framework.Assert; +import junit.framework.TestCase; + +/** + * Provides test cases for android.os.Build and, in turn, many of the + * system properties set by the build system. + */ +public class BuildTest extends TestCase { + + private static final String TAG = "BuildTest"; + + /** + * Asserts that a String is non-null and non-empty. If it is not, + * an AssertionFailedError is thrown with the given message. + */ + private static void assertNotEmpty(String message, String string) { + //Log.i(TAG, "" + message + ": " + string); + assertNotNull(message, string); + assertFalse(message, string.equals("")); + } + + /** + * Asserts that a String is non-null and non-empty. If it is not, + * an AssertionFailedError is thrown. + */ + private static void assertNotEmpty(String string) { + assertNotEmpty(null, string); + } + + /** + * Asserts that all android.os.Build fields are non-empty and/or in a valid range. + */ + @SmallTest + public void testBuildFields() throws Exception { + assertNotEmpty("ID", Build.ID); + assertNotEmpty("DISPLAY", Build.DISPLAY); + assertNotEmpty("PRODUCT", Build.PRODUCT); + assertNotEmpty("DEVICE", Build.DEVICE); + assertNotEmpty("BOARD", Build.BOARD); + assertNotEmpty("BRAND", Build.BRAND); + assertNotEmpty("MODEL", Build.MODEL); + assertNotEmpty("VERSION.INCREMENTAL", Build.VERSION.INCREMENTAL); + assertNotEmpty("VERSION.RELEASE", Build.VERSION.RELEASE); + assertNotEmpty("TYPE", Build.TYPE); + Assert.assertNotNull("TAGS", Build.TAGS); // TAGS is allowed to be empty. + assertNotEmpty("FINGERPRINT", Build.FINGERPRINT); + Assert.assertTrue("TIME", Build.TIME > 0); + assertNotEmpty("USER", Build.USER); + assertNotEmpty("HOST", Build.HOST); + + // TODO: if any of the android.os.Build fields have additional constraints + // (e.g., must be a C identifier, must be a valid filename, must not contain any spaces) + // add tests for them. + } +} diff --git a/src/main/java/android/os/Bundle.java b/src/main/java/android/os/Bundle.java new file mode 100644 index 0000000..c5c5372 --- /dev/null +++ b/src/main/java/android/os/Bundle.java @@ -0,0 +1,1064 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.util.ArrayMap; +import android.util.Size; +import android.util.SizeF; +import android.util.SparseArray; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * A mapping from String values to various Parcelable types. + * + */ +public final class Bundle extends BaseBundle implements Cloneable, Parcelable { + public static final Bundle EMPTY; + static final Parcel EMPTY_PARCEL; + + static { + EMPTY = new Bundle(); + EMPTY.mMap = ArrayMap.EMPTY; + EMPTY_PARCEL = BaseBundle.EMPTY_PARCEL; + } + + private boolean mHasFds = false; + private boolean mFdsKnown = true; + private boolean mAllowFds = true; + + /** + * Constructs a new, empty Bundle. + */ + public Bundle() { + super(); + } + + /** + * Constructs a Bundle whose data is stored as a Parcel. The data + * will be unparcelled on first contact, using the assigned ClassLoader. + * + * @param parcelledData a Parcel containing a Bundle + */ + Bundle(Parcel parcelledData) { + super(parcelledData); + + mHasFds = mParcelledData.hasFileDescriptors(); + mFdsKnown = true; + } + + /* package */ Bundle(Parcel parcelledData, int length) { + super(parcelledData, length); + + mHasFds = mParcelledData.hasFileDescriptors(); + mFdsKnown = true; + } + + /** + * Constructs a new, empty Bundle that uses a specific ClassLoader for + * instantiating Parcelable and Serializable objects. + * + * @param loader An explicit ClassLoader to use when instantiating objects + * inside of the Bundle. + */ + public Bundle(ClassLoader loader) { + super(loader); + } + + /** + * Constructs a new, empty Bundle sized to hold the given number of + * elements. The Bundle will grow as needed. + * + * @param capacity the initial capacity of the Bundle + */ + public Bundle(int capacity) { + super(capacity); + } + + /** + * Constructs a Bundle containing a copy of the mappings from the given + * Bundle. + * + * @param b a Bundle to be copied. + */ + public Bundle(Bundle b) { + super(b); + + mHasFds = b.mHasFds; + mFdsKnown = b.mFdsKnown; + } + + /** + * Constructs a Bundle containing a copy of the mappings from the given + * PersistableBundle. + * + * @param b a Bundle to be copied. + */ + public Bundle(PersistableBundle b) { + super(b); + } + + /** + * Make a Bundle for a single key/value pair. + * + * @hide + */ + public static Bundle forPair(String key, String value) { + Bundle b = new Bundle(1); + b.putString(key, value); + return b; + } + + /** + * Changes the ClassLoader this Bundle uses when instantiating objects. + * + * @param loader An explicit ClassLoader to use when instantiating objects + * inside of the Bundle. + */ + @Override + public void setClassLoader(ClassLoader loader) { + super.setClassLoader(loader); + } + + /** + * Return the ClassLoader currently associated with this Bundle. + */ + @Override + public ClassLoader getClassLoader() { + return super.getClassLoader(); + } + + /** @hide */ + public boolean setAllowFds(boolean allowFds) { + boolean orig = mAllowFds; + mAllowFds = allowFds; + return orig; + } + + /** + * Clones the current Bundle. The internal map is cloned, but the keys and + * values to which it refers are copied by reference. + */ + @Override + public Object clone() { + return new Bundle(this); + } + + /** + * Removes all elements from the mapping of this Bundle. + */ + @Override + public void clear() { + super.clear(); + + mHasFds = false; + mFdsKnown = true; + } + + /** + * Inserts all mappings from the given Bundle into this Bundle. + * + * @param bundle a Bundle + */ + public void putAll(Bundle bundle) { + unparcel(); + bundle.unparcel(); + mMap.putAll(bundle.mMap); + + // fd state is now known if and only if both bundles already knew + mHasFds |= bundle.mHasFds; + mFdsKnown = mFdsKnown && bundle.mFdsKnown; + } + + /** + * Reports whether the bundle contains any parcelled file descriptors. + */ + public boolean hasFileDescriptors() { + if (!mFdsKnown) { + boolean fdFound = false; // keep going until we find one or run out of data + + if (mParcelledData != null) { + if (mParcelledData.hasFileDescriptors()) { + fdFound = true; + } + } else { + // It's been unparcelled, so we need to walk the map + for (int i=mMap.size()-1; i>=0; i--) { + Object obj = mMap.valueAt(i); + if (obj instanceof Parcelable) { + if ((((Parcelable)obj).describeContents() + & Parcelable.CONTENTS_FILE_DESCRIPTOR) != 0) { + fdFound = true; + break; + } + } else if (obj instanceof Parcelable[]) { + Parcelable[] array = (Parcelable[]) obj; + for (int n = array.length - 1; n >= 0; n--) { + if ((array[n].describeContents() + & Parcelable.CONTENTS_FILE_DESCRIPTOR) != 0) { + fdFound = true; + break; + } + } + } else if (obj instanceof SparseArray) { + SparseArray array = + (SparseArray) obj; + for (int n = array.size() - 1; n >= 0; n--) { + if ((array.valueAt(n).describeContents() + & Parcelable.CONTENTS_FILE_DESCRIPTOR) != 0) { + fdFound = true; + break; + } + } + } else if (obj instanceof ArrayList) { + ArrayList array = (ArrayList) obj; + // an ArrayList here might contain either Strings or + // Parcelables; only look inside for Parcelables + if (!array.isEmpty() && (array.get(0) instanceof Parcelable)) { + for (int n = array.size() - 1; n >= 0; n--) { + Parcelable p = (Parcelable) array.get(n); + if (p != null && ((p.describeContents() + & Parcelable.CONTENTS_FILE_DESCRIPTOR) != 0)) { + fdFound = true; + break; + } + } + } + } + } + } + + mHasFds = fdFound; + mFdsKnown = true; + } + return mHasFds; + } + + /** + * Inserts a byte value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a byte + */ + @Override + public void putByte(String key, byte value) { + super.putByte(key, value); + } + + /** + * Inserts a char value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a char, or null + */ + @Override + public void putChar(String key, char value) { + super.putChar(key, value); + } + + /** + * Inserts a short value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a short + */ + @Override + public void putShort(String key, short value) { + super.putShort(key, value); + } + + /** + * Inserts a float value into the mapping of this Bundle, replacing + * any existing value for the given key. + * + * @param key a String, or null + * @param value a float + */ + @Override + public void putFloat(String key, float value) { + super.putFloat(key, value); + } + + /** + * Inserts a CharSequence value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a CharSequence, or null + */ + @Override + public void putCharSequence(String key, CharSequence value) { + super.putCharSequence(key, value); + } + + /** + * Inserts a Parcelable value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Parcelable object, or null + */ + public void putParcelable(String key, Parcelable value) { + unparcel(); + mMap.put(key, value); + mFdsKnown = false; + } + + /** + * Inserts a Size value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Size object, or null + */ + public void putSize(String key, Size value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts a SizeF value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a SizeF object, or null + */ + public void putSizeF(String key, SizeF value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an array of Parcelable values into the mapping of this Bundle, + * replacing any existing value for the given key. Either key or value may + * be null. + * + * @param key a String, or null + * @param value an array of Parcelable objects, or null + */ + public void putParcelableArray(String key, Parcelable[] value) { + unparcel(); + mMap.put(key, value); + mFdsKnown = false; + } + + /** + * Inserts a List of Parcelable values into the mapping of this Bundle, + * replacing any existing value for the given key. Either key or value may + * be null. + * + * @param key a String, or null + * @param value an ArrayList of Parcelable objects, or null + */ + public void putParcelableArrayList(String key, + ArrayList value) { + unparcel(); + mMap.put(key, value); + mFdsKnown = false; + } + + /** {@hide} */ + public void putParcelableList(String key, List value) { + unparcel(); + mMap.put(key, value); + mFdsKnown = false; + } + + /** + * Inserts a SparceArray of Parcelable values into the mapping of this + * Bundle, replacing any existing value for the given key. Either key + * or value may be null. + * + * @param key a String, or null + * @param value a SparseArray of Parcelable objects, or null + */ + public void putSparseParcelableArray(String key, + SparseArray value) { + unparcel(); + mMap.put(key, value); + mFdsKnown = false; + } + + /** + * Inserts an ArrayList value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList object, or null + */ + @Override + public void putIntegerArrayList(String key, ArrayList value) { + super.putIntegerArrayList(key, value); + } + + /** + * Inserts an ArrayList value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList object, or null + */ + @Override + public void putStringArrayList(String key, ArrayList value) { + super.putStringArrayList(key, value); + } + + /** + * Inserts an ArrayList value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an ArrayList object, or null + */ + @Override + public void putCharSequenceArrayList(String key, ArrayList value) { + super.putCharSequenceArrayList(key, value); + } + + /** + * Inserts a Serializable value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Serializable object, or null + */ + @Override + public void putSerializable(String key, Serializable value) { + super.putSerializable(key, value); + } + + /** + * Inserts a byte array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a byte array object, or null + */ + @Override + public void putByteArray(String key, byte[] value) { + super.putByteArray(key, value); + } + + /** + * Inserts a short array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a short array object, or null + */ + @Override + public void putShortArray(String key, short[] value) { + super.putShortArray(key, value); + } + + /** + * Inserts a char array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a char array object, or null + */ + @Override + public void putCharArray(String key, char[] value) { + super.putCharArray(key, value); + } + + /** + * Inserts a float array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a float array object, or null + */ + @Override + public void putFloatArray(String key, float[] value) { + super.putFloatArray(key, value); + } + + /** + * Inserts a CharSequence array value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a CharSequence array object, or null + */ + @Override + public void putCharSequenceArray(String key, CharSequence[] value) { + super.putCharSequenceArray(key, value); + } + + /** + * Inserts a Bundle value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Bundle object, or null + */ + public void putBundle(String key, Bundle value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an {@link IBinder} value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + *

You should be very careful when using this function. In many + * places where Bundles are used (such as inside of Intent objects), the Bundle + * can live longer inside of another process than the process that had originally + * created it. In that case, the IBinder you supply here will become invalid + * when your process goes away, and no longer usable, even if a new process is + * created for you later on.

+ * + * @param key a String, or null + * @param value an IBinder object, or null + */ + public void putBinder(String key, IBinder value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Inserts an IBinder value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value an IBinder object, or null + * + * @deprecated + * @hide This is the old name of the function. + */ + @Deprecated + public void putIBinder(String key, IBinder value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Returns the value associated with the given key, or (byte) 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a byte value + */ + @Override + public byte getByte(String key) { + return super.getByte(key); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a byte value + */ + @Override + public Byte getByte(String key, byte defaultValue) { + return super.getByte(key, defaultValue); + } + + /** + * Returns the value associated with the given key, or (char) 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a char value + */ + @Override + public char getChar(String key) { + return super.getChar(key); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a char value + */ + @Override + public char getChar(String key, char defaultValue) { + return super.getChar(key, defaultValue); + } + + /** + * Returns the value associated with the given key, or (short) 0 if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a short value + */ + @Override + public short getShort(String key) { + return super.getShort(key); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a short value + */ + @Override + public short getShort(String key, short defaultValue) { + return super.getShort(key, defaultValue); + } + + /** + * Returns the value associated with the given key, or 0.0f if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @return a float value + */ + @Override + public float getFloat(String key) { + return super.getFloat(key); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key. + * + * @param key a String + * @param defaultValue Value to return if key does not exist + * @return a float value + */ + @Override + public float getFloat(String key, float defaultValue) { + return super.getFloat(key, defaultValue); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a CharSequence value, or null + */ + @Override + public CharSequence getCharSequence(String key) { + return super.getCharSequence(key); + } + + /** + * Returns the value associated with the given key, or defaultValue if + * no mapping of the desired type exists for the given key or if a null + * value is explicitly associatd with the given key. + * + * @param key a String, or null + * @param defaultValue Value to return if key does not exist or if a null + * value is associated with the given key. + * @return the CharSequence value associated with the given key, or defaultValue + * if no valid CharSequence object is currently mapped to that key. + */ + @Override + public CharSequence getCharSequence(String key, CharSequence defaultValue) { + return super.getCharSequence(key, defaultValue); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Size value, or null + */ + public Size getSize(String key) { + unparcel(); + final Object o = mMap.get(key); + try { + return (Size) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Size", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Size value, or null + */ + public SizeF getSizeF(String key) { + unparcel(); + final Object o = mMap.get(key); + try { + return (SizeF) o; + } catch (ClassCastException e) { + typeWarning(key, o, "SizeF", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Bundle value, or null + */ + public Bundle getBundle(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (Bundle) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Bundle", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Parcelable value, or null + */ + public T getParcelable(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (T) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Parcelable", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Parcelable[] value, or null + */ + public Parcelable[] getParcelableArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (Parcelable[]) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Parcelable[]", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an ArrayList value, or null + */ + public ArrayList getParcelableArrayList(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (ArrayList) o; + } catch (ClassCastException e) { + typeWarning(key, o, "ArrayList", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * + * @return a SparseArray of T values, or null + */ + public SparseArray getSparseParcelableArray(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (SparseArray) o; + } catch (ClassCastException e) { + typeWarning(key, o, "SparseArray", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Serializable value, or null + */ + @Override + public Serializable getSerializable(String key) { + return super.getSerializable(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an ArrayList value, or null + */ + @Override + public ArrayList getIntegerArrayList(String key) { + return super.getIntegerArrayList(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an ArrayList value, or null + */ + @Override + public ArrayList getStringArrayList(String key) { + return super.getStringArrayList(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an ArrayList value, or null + */ + @Override + public ArrayList getCharSequenceArrayList(String key) { + return super.getCharSequenceArrayList(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a byte[] value, or null + */ + @Override + public byte[] getByteArray(String key) { + return super.getByteArray(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a short[] value, or null + */ + @Override + public short[] getShortArray(String key) { + return super.getShortArray(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a char[] value, or null + */ + @Override + public char[] getCharArray(String key) { + return super.getCharArray(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a float[] value, or null + */ + @Override + public float[] getFloatArray(String key) { + return super.getFloatArray(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a CharSequence[] value, or null + */ + @Override + public CharSequence[] getCharSequenceArray(String key) { + return super.getCharSequenceArray(key); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an IBinder value, or null + */ + public IBinder getBinder(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (IBinder) o; + } catch (ClassCastException e) { + typeWarning(key, o, "IBinder", e); + return null; + } + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return an IBinder value, or null + * + * @deprecated + * @hide This is the old name of the function. + */ + @Deprecated + public IBinder getIBinder(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (IBinder) o; + } catch (ClassCastException e) { + typeWarning(key, o, "IBinder", e); + return null; + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Bundle createFromParcel(Parcel in) { + return in.readBundle(); + } + + @Override + public Bundle[] newArray(int size) { + return new Bundle[size]; + } + }; + + /** + * Report the nature of this Parcelable's contents + */ + @Override + public int describeContents() { + int mask = 0; + if (hasFileDescriptors()) { + mask |= Parcelable.CONTENTS_FILE_DESCRIPTOR; + } + return mask; + } + + /** + * Writes the Bundle contents to a Parcel, typically in order for + * it to be passed through an IBinder connection. + * @param parcel The parcel to copy this bundle to. + */ + @Override + public void writeToParcel(Parcel parcel, int flags) { + final boolean oldAllowFds = parcel.pushAllowFds(mAllowFds); + try { + super.writeToParcelInner(parcel, flags); + } finally { + parcel.restoreAllowFds(oldAllowFds); + } + } + + /** + * Reads the Parcel contents into this Bundle, typically in order for + * it to be passed through an IBinder connection. + * @param parcel The parcel to overwrite this bundle from. + */ + public void readFromParcel(Parcel parcel) { + super.readFromParcelInner(parcel); + mHasFds = mParcelledData.hasFileDescriptors(); + mFdsKnown = true; + } + + @Override + public synchronized String toString() { + if (mParcelledData != null) { + if (mParcelledData == EMPTY_PARCEL) { + return "Bundle[EMPTY_PARCEL]"; + } else { + return "Bundle[mParcelledData.dataSize=" + + mParcelledData.dataSize() + "]"; + } + } + return "Bundle[" + mMap.toString() + "]"; + } + +} diff --git a/src/main/java/android/os/CancellationSignal.java b/src/main/java/android/os/CancellationSignal.java new file mode 100644 index 0000000..e8053d5 --- /dev/null +++ b/src/main/java/android/os/CancellationSignal.java @@ -0,0 +1,209 @@ +/* + * 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 android.os; + +import android.os.ICancellationSignal; + +/** + * Provides the ability to cancel an operation in progress. + */ +public final class CancellationSignal { + private boolean mIsCanceled; + private OnCancelListener mOnCancelListener; + private ICancellationSignal mRemote; + private boolean mCancelInProgress; + + /** + * Creates a cancellation signal, initially not canceled. + */ + public CancellationSignal() { + } + + /** + * Returns true if the operation has been canceled. + * + * @return True if the operation has been canceled. + */ + public boolean isCanceled() { + synchronized (this) { + return mIsCanceled; + } + } + + /** + * Throws {@link OperationCanceledException} if the operation has been canceled. + * + * @throws OperationCanceledException if the operation has been canceled. + */ + public void throwIfCanceled() { + if (isCanceled()) { + throw new OperationCanceledException(); + } + } + + /** + * Cancels the operation and signals the cancellation listener. + * If the operation has not yet started, then it will be canceled as soon as it does. + */ + public void cancel() { + final OnCancelListener listener; + final ICancellationSignal remote; + synchronized (this) { + if (mIsCanceled) { + return; + } + mIsCanceled = true; + mCancelInProgress = true; + listener = mOnCancelListener; + remote = mRemote; + } + + try { + if (listener != null) { + listener.onCancel(); + } + if (remote != null) { + try { + remote.cancel(); + } catch (RemoteException ex) { + } + } + } finally { + synchronized (this) { + mCancelInProgress = false; + notifyAll(); + } + } + } + + /** + * Sets the cancellation listener to be called when canceled. + * + * This method is intended to be used by the recipient of a cancellation signal + * such as a database or a content provider to handle cancellation requests + * while performing a long-running operation. This method is not intended to be + * used by applications themselves. + * + * If {@link CancellationSignal#cancel} has already been called, then the provided + * listener is invoked immediately. + * + * This method is guaranteed that the listener will not be called after it + * has been removed. + * + * @param listener The cancellation listener, or null to remove the current listener. + */ + public void setOnCancelListener(OnCancelListener listener) { + synchronized (this) { + waitForCancelFinishedLocked(); + + if (mOnCancelListener == listener) { + return; + } + mOnCancelListener = listener; + if (!mIsCanceled || listener == null) { + return; + } + } + listener.onCancel(); + } + + /** + * Sets the remote transport. + * + * If {@link CancellationSignal#cancel} has already been called, then the provided + * remote transport is canceled immediately. + * + * This method is guaranteed that the remote transport will not be called after it + * has been removed. + * + * @param remote The remote transport, or null to remove. + * + * @hide + */ + public void setRemote(ICancellationSignal remote) { + synchronized (this) { + waitForCancelFinishedLocked(); + + if (mRemote == remote) { + return; + } + mRemote = remote; + if (!mIsCanceled || remote == null) { + return; + } + } + try { + remote.cancel(); + } catch (RemoteException ex) { + } + } + + private void waitForCancelFinishedLocked() { + while (mCancelInProgress) { + try { + wait(); + } catch (InterruptedException ex) { + } + } + } + + /** + * Creates a transport that can be returned back to the caller of + * a Binder function and subsequently used to dispatch a cancellation signal. + * + * @return The new cancellation signal transport. + * + * @hide + */ + public static ICancellationSignal createTransport() { + return new Transport(); + } + + /** + * Given a locally created transport, returns its associated cancellation signal. + * + * @param transport The locally created transport, or null if none. + * @return The associated cancellation signal, or null if none. + * + * @hide + */ + public static CancellationSignal fromTransport(ICancellationSignal transport) { + if (transport instanceof Transport) { + return ((Transport)transport).mCancellationSignal; + } + return null; + } + + /** + * Listens for cancellation. + */ + public interface OnCancelListener { + /** + * Called when {@link CancellationSignal#cancel} is invoked. + */ + void onCancel(); + } + + private static final class Transport extends ICancellationSignal.Stub { + final CancellationSignal mCancellationSignal = new CancellationSignal(); + + @Override + public void cancel() throws RemoteException { + mCancellationSignal.cancel(); + } + } +} diff --git a/src/main/java/android/os/CommonClock.java b/src/main/java/android/os/CommonClock.java new file mode 100644 index 0000000..f83a90b --- /dev/null +++ b/src/main/java/android/os/CommonClock.java @@ -0,0 +1,406 @@ +/* + * 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 android.os; + +import java.net.InetSocketAddress; +import java.util.NoSuchElementException; +import android.os.Binder; +import android.os.CommonTimeUtils; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.ServiceManager; +import static android.system.OsConstants.*; + +/** + * Used for accessing the android common time service's common clock and receiving notifications + * about common time synchronization status changes. + * @hide + */ +public class CommonClock { + /** + * Sentinel value returned by {@link #getTime()} and {@link #getEstimatedError()} when the + * common time service is not able to determine the current common time due to a lack of + * synchronization. + */ + public static final long TIME_NOT_SYNCED = -1; + + /** + * Sentinel value returned by {@link #getTimelineId()} when the common time service is not + * currently synced to any timeline. + */ + public static final long INVALID_TIMELINE_ID = 0; + + /** + * Sentinel value returned by {@link #getEstimatedError()} when the common time service is not + * currently synced to any timeline. + */ + public static final int ERROR_ESTIMATE_UNKNOWN = 0x7FFFFFFF; + + /** + * Value used by {@link #getState()} to indicate that there was an internal error while + * attempting to determine the state of the common time service. + */ + public static final int STATE_INVALID = -1; + + /** + * Value used by {@link #getState()} to indicate that the common time service is in its initial + * state and attempting to find the current timeline master, if any. The service will + * transition to either {@link #STATE_CLIENT} if it finds an active master, or to + * {@link #STATE_MASTER} if no active master is found and this client becomes the master of a + * new timeline. + */ + public static final int STATE_INITIAL = 0; + + /** + * Value used by {@link #getState()} to indicate that the common time service is in its client + * state and is synchronizing its time to a different timeline master on the network. + */ + public static final int STATE_CLIENT = 1; + + /** + * Value used by {@link #getState()} to indicate that the common time service is in its master + * state and is serving as the timeline master for other common time service clients on the + * network. + */ + public static final int STATE_MASTER = 2; + + /** + * Value used by {@link #getState()} to indicate that the common time service is in its Ronin + * state. Common time service instances in the client state enter the Ronin state after their + * timeline master becomes unreachable on the network. Common time services who enter the Ronin + * state will begin a new master election for the timeline they were recently clients of. As + * clients detect they are not the winner and drop out of the election, they will transition to + * the {@link #STATE_WAIT_FOR_ELECTION} state. When there is only one client remaining in the + * election, it will assume ownership of the timeline and transition to the + * {@link #STATE_MASTER} state. During the election, all clients will allow their timeline to + * drift without applying correction. + */ + public static final int STATE_RONIN = 3; + + /** + * Value used by {@link #getState()} to indicate that the common time service is waiting for a + * master election to conclude and for the new master to announce itself before transitioning to + * the {@link #STATE_CLIENT} state. If no new master announces itself within the timeout + * threshold, the time service will transition back to the {@link #STATE_RONIN} state in order + * to restart the election. + */ + public static final int STATE_WAIT_FOR_ELECTION = 4; + + /** + * Name of the underlying native binder service + */ + public static final String SERVICE_NAME = "common_time.clock"; + + /** + * Class constructor. + * @throws android.os.RemoteException + */ + public CommonClock() + throws RemoteException { + mRemote = ServiceManager.getService(SERVICE_NAME); + if (null == mRemote) + throw new RemoteException(); + + mInterfaceDesc = mRemote.getInterfaceDescriptor(); + mUtils = new CommonTimeUtils(mRemote, mInterfaceDesc); + mRemote.linkToDeath(mDeathHandler, 0); + registerTimelineChangeListener(); + } + + /** + * Handy class factory method. + */ + static public CommonClock create() { + CommonClock retVal; + + try { + retVal = new CommonClock(); + } + catch (RemoteException e) { + retVal = null; + } + + return retVal; + } + + /** + * Release all native resources held by this {@link android.os.CommonClock} instance. Once + * resources have been released, the {@link android.os.CommonClock} instance is disconnected from + * the native service and will throw a {@link android.os.RemoteException} if any of its + * methods are called. Clients should always call release on their client instances before + * releasing their last Java reference to the instance. Failure to do this will cause + * non-deterministic native resource reclamation and may cause the common time service to remain + * active on the network for longer than it should. + */ + public void release() { + unregisterTimelineChangeListener(); + if (null != mRemote) { + try { + mRemote.unlinkToDeath(mDeathHandler, 0); + } + catch (NoSuchElementException e) { } + mRemote = null; + } + mUtils = null; + } + + /** + * Gets the common clock's current time. + * + * @return a signed 64-bit value representing the current common time in microseconds, or the + * special value {@link #TIME_NOT_SYNCED} if the common time service is currently not + * synchronized. + * @throws android.os.RemoteException + */ + public long getTime() + throws RemoteException { + throwOnDeadServer(); + return mUtils.transactGetLong(METHOD_GET_COMMON_TIME, TIME_NOT_SYNCED); + } + + /** + * Gets the current estimation of common clock's synchronization accuracy from the common time + * service. + * + * @return a signed 32-bit value representing the common time service's estimation of + * synchronization accuracy in microseconds, or the special value + * {@link #ERROR_ESTIMATE_UNKNOWN} if the common time service is currently not synchronized. + * Negative values indicate that the local server estimates that the nominal common time is + * behind the local server's time (in other words, the local clock is running fast) Positive + * values indicate that the local server estimates that the nominal common time is ahead of the + * local server's time (in other words, the local clock is running slow) + * @throws android.os.RemoteException + */ + public int getEstimatedError() + throws RemoteException { + throwOnDeadServer(); + return mUtils.transactGetInt(METHOD_GET_ESTIMATED_ERROR, ERROR_ESTIMATE_UNKNOWN); + } + + /** + * Gets the ID of the timeline the common time service is currently synchronizing its clock to. + * + * @return a long representing the unique ID of the timeline the common time service is + * currently synchronizing with, or {@link #INVALID_TIMELINE_ID} if the common time service is + * currently not synchronized. + * @throws android.os.RemoteException + */ + public long getTimelineId() + throws RemoteException { + throwOnDeadServer(); + return mUtils.transactGetLong(METHOD_GET_TIMELINE_ID, INVALID_TIMELINE_ID); + } + + /** + * Gets the current state of this clock's common time service in the the master election + * algorithm. + * + * @return a integer indicating the current state of the this clock's common time service in the + * master election algorithm or {@link #STATE_INVALID} if there is an internal error. + * @throws android.os.RemoteException + */ + public int getState() + throws RemoteException { + throwOnDeadServer(); + return mUtils.transactGetInt(METHOD_GET_STATE, STATE_INVALID); + } + + /** + * Gets the IP address and UDP port of the current timeline master. + * + * @return an InetSocketAddress containing the IP address and UDP port of the current timeline + * master, or null if there is no current master. + * @throws android.os.RemoteException + */ + public InetSocketAddress getMasterAddr() + throws RemoteException { + throwOnDeadServer(); + return mUtils.transactGetSockaddr(METHOD_GET_MASTER_ADDRESS); + } + + /** + * The OnTimelineChangedListener interface defines a method called by the + * {@link android.os.CommonClock} instance to indicate that the time synchronization service has + * either synchronized with a new timeline, or is no longer a member of any timeline. The + * client application can implement this interface and register the listener with the + * {@link #setTimelineChangedListener(OnTimelineChangedListener)} method. + */ + public interface OnTimelineChangedListener { + /** + * Method called when the time service's timeline has changed. + * + * @param newTimelineId a long which uniquely identifies the timeline the time + * synchronization service is now a member of, or {@link #INVALID_TIMELINE_ID} if the the + * service is not synchronized to any timeline. + */ + void onTimelineChanged(long newTimelineId); + } + + /** + * Registers an OnTimelineChangedListener interface. + *

Call this method with a null listener to stop receiving server death notifications. + */ + public void setTimelineChangedListener(OnTimelineChangedListener listener) { + synchronized (mListenerLock) { + mTimelineChangedListener = listener; + } + } + + /** + * The OnServerDiedListener interface defines a method called by the + * {@link android.os.CommonClock} instance to indicate that the connection to the native media + * server has been broken and that the {@link android.os.CommonClock} instance will need to be + * released and re-created. The client application can implement this interface and register + * the listener with the {@link #setServerDiedListener(OnServerDiedListener)} method. + */ + public interface OnServerDiedListener { + /** + * Method called when the native media server has died.

If the native common time + * service encounters a fatal error and needs to restart, the binder connection from the + * {@link android.os.CommonClock} instance to the common time service will be broken. To + * restore functionality, clients should {@link #release()} their old visualizer and create + * a new instance. + */ + void onServerDied(); + } + + /** + * Registers an OnServerDiedListener interface. + *

Call this method with a null listener to stop receiving server death notifications. + */ + public void setServerDiedListener(OnServerDiedListener listener) { + synchronized (mListenerLock) { + mServerDiedListener = listener; + } + } + + protected void finalize() throws Throwable { release(); } + + private void throwOnDeadServer() throws RemoteException { + if ((null == mRemote) || (null == mUtils)) + throw new RemoteException(); + } + + private final Object mListenerLock = new Object(); + private OnTimelineChangedListener mTimelineChangedListener = null; + private OnServerDiedListener mServerDiedListener = null; + + private IBinder mRemote = null; + private String mInterfaceDesc = ""; + private CommonTimeUtils mUtils; + + private IBinder.DeathRecipient mDeathHandler = new IBinder.DeathRecipient() { + public void binderDied() { + synchronized (mListenerLock) { + if (null != mServerDiedListener) + mServerDiedListener.onServerDied(); + } + } + }; + + private class TimelineChangedListener extends Binder { + @Override + protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + switch (code) { + case METHOD_CBK_ON_TIMELINE_CHANGED: + data.enforceInterface(DESCRIPTOR); + long timelineId = data.readLong(); + synchronized (mListenerLock) { + if (null != mTimelineChangedListener) + mTimelineChangedListener.onTimelineChanged(timelineId); + } + return true; + } + + return super.onTransact(code, data, reply, flags); + } + + private static final String DESCRIPTOR = "android.os.ICommonClockListener"; + }; + + private TimelineChangedListener mCallbackTgt = null; + + private void registerTimelineChangeListener() throws RemoteException { + if (null != mCallbackTgt) + return; + + boolean success = false; + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + mCallbackTgt = new TimelineChangedListener(); + + try { + data.writeInterfaceToken(mInterfaceDesc); + data.writeStrongBinder(mCallbackTgt); + mRemote.transact(METHOD_REGISTER_LISTENER, data, reply, 0); + success = (0 == reply.readInt()); + } + catch (RemoteException e) { + success = false; + } + finally { + reply.recycle(); + data.recycle(); + } + + // Did we catch a remote exception or fail to register our callback target? If so, our + // object must already be dead (or be as good as dead). Clear out all of our state so that + // our other methods will properly indicate a dead object. + if (!success) { + mCallbackTgt = null; + mRemote = null; + mUtils = null; + } + } + + private void unregisterTimelineChangeListener() { + if (null == mCallbackTgt) + return; + + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + + try { + data.writeInterfaceToken(mInterfaceDesc); + data.writeStrongBinder(mCallbackTgt); + mRemote.transact(METHOD_UNREGISTER_LISTENER, data, reply, 0); + } + catch (RemoteException e) { } + finally { + reply.recycle(); + data.recycle(); + mCallbackTgt = null; + } + } + + private static final int METHOD_IS_COMMON_TIME_VALID = IBinder.FIRST_CALL_TRANSACTION; + private static final int METHOD_COMMON_TIME_TO_LOCAL_TIME = METHOD_IS_COMMON_TIME_VALID + 1; + private static final int METHOD_LOCAL_TIME_TO_COMMON_TIME = METHOD_COMMON_TIME_TO_LOCAL_TIME + 1; + private static final int METHOD_GET_COMMON_TIME = METHOD_LOCAL_TIME_TO_COMMON_TIME + 1; + private static final int METHOD_GET_COMMON_FREQ = METHOD_GET_COMMON_TIME + 1; + private static final int METHOD_GET_LOCAL_TIME = METHOD_GET_COMMON_FREQ + 1; + private static final int METHOD_GET_LOCAL_FREQ = METHOD_GET_LOCAL_TIME + 1; + private static final int METHOD_GET_ESTIMATED_ERROR = METHOD_GET_LOCAL_FREQ + 1; + private static final int METHOD_GET_TIMELINE_ID = METHOD_GET_ESTIMATED_ERROR + 1; + private static final int METHOD_GET_STATE = METHOD_GET_TIMELINE_ID + 1; + private static final int METHOD_GET_MASTER_ADDRESS = METHOD_GET_STATE + 1; + private static final int METHOD_REGISTER_LISTENER = METHOD_GET_MASTER_ADDRESS + 1; + private static final int METHOD_UNREGISTER_LISTENER = METHOD_REGISTER_LISTENER + 1; + + private static final int METHOD_CBK_ON_TIMELINE_CHANGED = IBinder.FIRST_CALL_TRANSACTION; +} diff --git a/src/main/java/android/os/CommonTimeConfig.java b/src/main/java/android/os/CommonTimeConfig.java new file mode 100644 index 0000000..1f9fab5 --- /dev/null +++ b/src/main/java/android/os/CommonTimeConfig.java @@ -0,0 +1,447 @@ +/* + * 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 android.os; + +import java.net.InetSocketAddress; +import java.util.NoSuchElementException; + +import android.os.CommonTimeUtils; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; + +/** + * Used for configuring and controlling the status of the android common time service. + * @hide + */ +public class CommonTimeConfig { + /** + * Successful operation. + */ + public static final int SUCCESS = 0; + /** + * Unspecified error. + */ + public static final int ERROR = -1; + /** + * Operation failed due to bad parameter value. + */ + public static final int ERROR_BAD_VALUE = -4; + /** + * Operation failed due to dead remote object. + */ + public static final int ERROR_DEAD_OBJECT = -7; + + /** + * Sentinel value returned by {@link #getMasterElectionGroupId()} when an error occurs trying to + * fetch the master election group. + */ + public static final long INVALID_GROUP_ID = -1; + + /** + * Name of the underlying native binder service + */ + public static final String SERVICE_NAME = "common_time.config"; + + /** + * Class constructor. + * @throws android.os.RemoteException + */ + public CommonTimeConfig() + throws RemoteException { + mRemote = ServiceManager.getService(SERVICE_NAME); + if (null == mRemote) + throw new RemoteException(); + + mInterfaceDesc = mRemote.getInterfaceDescriptor(); + mUtils = new CommonTimeUtils(mRemote, mInterfaceDesc); + mRemote.linkToDeath(mDeathHandler, 0); + } + + /** + * Handy class factory method. + */ + static public CommonTimeConfig create() { + CommonTimeConfig retVal; + + try { + retVal = new CommonTimeConfig(); + } + catch (RemoteException e) { + retVal = null; + } + + return retVal; + } + + /** + * Release all native resources held by this {@link android.os.CommonTimeConfig} instance. Once + * resources have been released, the {@link android.os.CommonTimeConfig} instance is + * disconnected from the native service and will throw a {@link android.os.RemoteException} if + * any of its methods are called. Clients should always call release on their client instances + * before releasing their last Java reference to the instance. Failure to do this will cause + * non-deterministic native resource reclamation and may cause the common time service to remain + * active on the network for longer than it should. + */ + public void release() { + if (null != mRemote) { + try { + mRemote.unlinkToDeath(mDeathHandler, 0); + } + catch (NoSuchElementException e) { } + mRemote = null; + } + mUtils = null; + } + + /** + * Gets the current priority of the common time service used in the master election protocol. + * + * @return an 8 bit value indicating the priority of this common time service relative to other + * common time services operating in the same domain. + * @throws android.os.RemoteException + */ + public byte getMasterElectionPriority() + throws RemoteException { + throwOnDeadServer(); + return (byte)mUtils.transactGetInt(METHOD_GET_MASTER_ELECTION_PRIORITY, -1); + } + + /** + * Sets the current priority of the common time service used in the master election protocol. + * + * @param priority priority of the common time service used in the master election protocol. + * Lower numbers are lower priority. + * @return {@link #SUCCESS} in case of success, + * {@link #ERROR} or {@link #ERROR_DEAD_OBJECT} in case of failure. + */ + public int setMasterElectionPriority(byte priority) { + if (checkDeadServer()) + return ERROR_DEAD_OBJECT; + return mUtils.transactSetInt(METHOD_SET_MASTER_ELECTION_PRIORITY, priority); + } + + /** + * Gets the IP endpoint used by the time service to participate in the master election protocol. + * + * @return an InetSocketAddress containing the IP address and UDP port being used by the + * system's common time service to participate in the master election protocol. + * @throws android.os.RemoteException + */ + public InetSocketAddress getMasterElectionEndpoint() + throws RemoteException { + throwOnDeadServer(); + return mUtils.transactGetSockaddr(METHOD_GET_MASTER_ELECTION_ENDPOINT); + } + + /** + * Sets the IP endpoint used by the common time service to participate in the master election + * protocol. + * + * @param ep The IP address and UDP port to be used by the common time service to participate in + * the master election protocol. The supplied IP address must be either the broadcast or + * multicast address, unicast addresses are considered to be illegal values. + * @return {@link #SUCCESS} in case of success, + * {@link #ERROR}, {@link #ERROR_BAD_VALUE} or {@link #ERROR_DEAD_OBJECT} in case of failure. + */ + public int setMasterElectionEndpoint(InetSocketAddress ep) { + if (checkDeadServer()) + return ERROR_DEAD_OBJECT; + return mUtils.transactSetSockaddr(METHOD_SET_MASTER_ELECTION_ENDPOINT, ep); + } + + /** + * Gets the current group ID used by the common time service in the master election protocol. + * + * @return The 64-bit group ID of the common time service. + * @throws android.os.RemoteException + */ + public long getMasterElectionGroupId() + throws RemoteException { + throwOnDeadServer(); + return mUtils.transactGetLong(METHOD_GET_MASTER_ELECTION_GROUP_ID, INVALID_GROUP_ID); + } + + /** + * Sets the current group ID used by the common time service in the master election protocol. + * + * @param id The 64-bit group ID of the common time service. + * @return {@link #SUCCESS} in case of success, + * {@link #ERROR}, {@link #ERROR_BAD_VALUE} or {@link #ERROR_DEAD_OBJECT} in case of failure. + */ + public int setMasterElectionGroupId(long id) { + if (checkDeadServer()) + return ERROR_DEAD_OBJECT; + return mUtils.transactSetLong(METHOD_SET_MASTER_ELECTION_GROUP_ID, id); + } + + /** + * Gets the name of the network interface which the common time service attempts to bind to. + * + * @return a string with the network interface name which the common time service is bound to, + * or null if the service is currently unbound. Examples of interface names are things like + * "eth0", or "wlan0". + * @throws android.os.RemoteException + */ + public String getInterfaceBinding() + throws RemoteException { + throwOnDeadServer(); + + String ifaceName = mUtils.transactGetString(METHOD_GET_INTERFACE_BINDING, null); + + if ((null != ifaceName) && (0 == ifaceName.length())) + return null; + + return ifaceName; + } + + /** + * Sets the name of the network interface which the common time service should attempt to bind + * to. + * + * @param ifaceName The name of the network interface ("eth0", "wlan0", etc...) wich the common + * time service should attempt to bind to, or null to force the common time service to unbind + * from the network and run in networkless mode. + * @return {@link #SUCCESS} in case of success, + * {@link #ERROR}, {@link #ERROR_BAD_VALUE} or {@link #ERROR_DEAD_OBJECT} in case of failure. + */ + public int setNetworkBinding(String ifaceName) { + if (checkDeadServer()) + return ERROR_DEAD_OBJECT; + + return mUtils.transactSetString(METHOD_SET_INTERFACE_BINDING, + (null == ifaceName) ? "" : ifaceName); + } + + /** + * Gets the amount of time the common time service will wait between master announcements when + * it is the timeline master. + * + * @return The time (in milliseconds) between master announcements. + * @throws android.os.RemoteException + */ + public int getMasterAnnounceInterval() + throws RemoteException { + throwOnDeadServer(); + return mUtils.transactGetInt(METHOD_GET_MASTER_ANNOUNCE_INTERVAL, -1); + } + + /** + * Sets the amount of time the common time service will wait between master announcements when + * it is the timeline master. + * + * @param interval The time (in milliseconds) between master announcements. + * @return {@link #SUCCESS} in case of success, + * {@link #ERROR}, {@link #ERROR_BAD_VALUE} or {@link #ERROR_DEAD_OBJECT} in case of failure. + */ + public int setMasterAnnounceInterval(int interval) { + if (checkDeadServer()) + return ERROR_DEAD_OBJECT; + return mUtils.transactSetInt(METHOD_SET_MASTER_ANNOUNCE_INTERVAL, interval); + } + + /** + * Gets the amount of time the common time service will wait between time synchronization + * requests when it is the client of another common time service on the network. + * + * @return The time (in milliseconds) between time sync requests. + * @throws android.os.RemoteException + */ + public int getClientSyncInterval() + throws RemoteException { + throwOnDeadServer(); + return mUtils.transactGetInt(METHOD_GET_CLIENT_SYNC_INTERVAL, -1); + } + + /** + * Sets the amount of time the common time service will wait between time synchronization + * requests when it is the client of another common time service on the network. + * + * @param interval The time (in milliseconds) between time sync requests. + * @return {@link #SUCCESS} in case of success, + * {@link #ERROR}, {@link #ERROR_BAD_VALUE} or {@link #ERROR_DEAD_OBJECT} in case of failure. + */ + public int setClientSyncInterval(int interval) { + if (checkDeadServer()) + return ERROR_DEAD_OBJECT; + return mUtils.transactSetInt(METHOD_SET_CLIENT_SYNC_INTERVAL, interval); + } + + /** + * Gets the panic threshold for the estimated error level of the common time service. When the + * common time service's estimated error rises above this level, the service will panic and + * reset, causing a discontinuity in the currently synchronized timeline. + * + * @return The threshold (in microseconds) past which the common time service will panic. + * @throws android.os.RemoteException + */ + public int getPanicThreshold() + throws RemoteException { + throwOnDeadServer(); + return mUtils.transactGetInt(METHOD_GET_PANIC_THRESHOLD, -1); + } + + /** + * Sets the panic threshold for the estimated error level of the common time service. When the + * common time service's estimated error rises above this level, the service will panic and + * reset, causing a discontinuity in the currently synchronized timeline. + * + * @param threshold The threshold (in microseconds) past which the common time service will + * panic. + * @return {@link #SUCCESS} in case of success, + * {@link #ERROR}, {@link #ERROR_BAD_VALUE} or {@link #ERROR_DEAD_OBJECT} in case of failure. + */ + public int setPanicThreshold(int threshold) { + if (checkDeadServer()) + return ERROR_DEAD_OBJECT; + return mUtils.transactSetInt(METHOD_SET_PANIC_THRESHOLD, threshold); + } + + /** + * Gets the current state of the common time service's auto disable flag. + * + * @return The current state of the common time service's auto disable flag. + * @throws android.os.RemoteException + */ + public boolean getAutoDisable() + throws RemoteException { + throwOnDeadServer(); + return (1 == mUtils.transactGetInt(METHOD_GET_AUTO_DISABLE, 1)); + } + + /** + * Sets the current state of the common time service's auto disable flag. When the time + * service's auto disable flag is set, it will automatically cease all network activity when + * it has no active local clients, resuming activity the next time the service has interested + * local clients. When the auto disabled flag is cleared, the common time service will continue + * to participate the time synchronization group even when it has no active local clients. + * + * @param autoDisable The desired state of the common time service's auto disable flag. + * @return {@link #SUCCESS} in case of success, + * {@link #ERROR} or {@link #ERROR_DEAD_OBJECT} in case of failure. + */ + public int setAutoDisable(boolean autoDisable) { + if (checkDeadServer()) + return ERROR_DEAD_OBJECT; + + return mUtils.transactSetInt(METHOD_SET_AUTO_DISABLE, autoDisable ? 1 : 0); + } + + /** + * At startup, the time service enters the initial state and remains there until it is given a + * network interface to bind to. Common time will be unavailable to clients of the common time + * service until the service joins a network (even an empty network). Devices may use the + * {@link #forceNetworklessMasterMode()} method to force a time service in the INITIAL state + * with no network configuration to assume MASTER status for a brand new timeline in order to + * allow clients of the common time service to operate, even though the device is isolated and + * not on any network. When a networkless master does join a network, it will defer to any + * masters already on the network, or continue to maintain the timeline it made up during its + * networkless state if no other masters are detected. Attempting to force a client into master + * mode while it is actively bound to a network will fail with the status code {@link #ERROR} + * + * @return {@link #SUCCESS} in case of success, + * {@link #ERROR} or {@link #ERROR_DEAD_OBJECT} in case of failure. + */ + public int forceNetworklessMasterMode() { + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + + try { + data.writeInterfaceToken(mInterfaceDesc); + mRemote.transact(METHOD_FORCE_NETWORKLESS_MASTER_MODE, data, reply, 0); + + return reply.readInt(); + } + catch (RemoteException e) { + return ERROR_DEAD_OBJECT; + } + finally { + reply.recycle(); + data.recycle(); + } + } + + /** + * The OnServerDiedListener interface defines a method called by the + * {@link android.os.CommonTimeConfig} instance to indicate that the connection to the native + * media server has been broken and that the {@link android.os.CommonTimeConfig} instance will + * need to be released and re-created. The client application can implement this interface and + * register the listener with the {@link #setServerDiedListener(OnServerDiedListener)} method. + */ + public interface OnServerDiedListener { + /** + * Method called when the native common time service has died.

If the native common time + * service encounters a fatal error and needs to restart, the binder connection from the + * {@link android.os.CommonTimeConfig} instance to the common time service will be broken. + */ + void onServerDied(); + } + + /** + * Registers an OnServerDiedListener interface. + *

Call this method with a null listener to stop receiving server death notifications. + */ + public void setServerDiedListener(OnServerDiedListener listener) { + synchronized (mListenerLock) { + mServerDiedListener = listener; + } + } + + protected void finalize() throws Throwable { release(); } + + private boolean checkDeadServer() { + return ((null == mRemote) || (null == mUtils)); + } + + private void throwOnDeadServer() throws RemoteException { + if (checkDeadServer()) + throw new RemoteException(); + } + + private final Object mListenerLock = new Object(); + private OnServerDiedListener mServerDiedListener = null; + + private IBinder mRemote = null; + private String mInterfaceDesc = ""; + private CommonTimeUtils mUtils; + + private IBinder.DeathRecipient mDeathHandler = new IBinder.DeathRecipient() { + public void binderDied() { + synchronized (mListenerLock) { + if (null != mServerDiedListener) + mServerDiedListener.onServerDied(); + } + } + }; + + private static final int METHOD_GET_MASTER_ELECTION_PRIORITY = IBinder.FIRST_CALL_TRANSACTION; + private static final int METHOD_SET_MASTER_ELECTION_PRIORITY = METHOD_GET_MASTER_ELECTION_PRIORITY + 1; + private static final int METHOD_GET_MASTER_ELECTION_ENDPOINT = METHOD_SET_MASTER_ELECTION_PRIORITY + 1; + private static final int METHOD_SET_MASTER_ELECTION_ENDPOINT = METHOD_GET_MASTER_ELECTION_ENDPOINT + 1; + private static final int METHOD_GET_MASTER_ELECTION_GROUP_ID = METHOD_SET_MASTER_ELECTION_ENDPOINT + 1; + private static final int METHOD_SET_MASTER_ELECTION_GROUP_ID = METHOD_GET_MASTER_ELECTION_GROUP_ID + 1; + private static final int METHOD_GET_INTERFACE_BINDING = METHOD_SET_MASTER_ELECTION_GROUP_ID + 1; + private static final int METHOD_SET_INTERFACE_BINDING = METHOD_GET_INTERFACE_BINDING + 1; + private static final int METHOD_GET_MASTER_ANNOUNCE_INTERVAL = METHOD_SET_INTERFACE_BINDING + 1; + private static final int METHOD_SET_MASTER_ANNOUNCE_INTERVAL = METHOD_GET_MASTER_ANNOUNCE_INTERVAL + 1; + private static final int METHOD_GET_CLIENT_SYNC_INTERVAL = METHOD_SET_MASTER_ANNOUNCE_INTERVAL + 1; + private static final int METHOD_SET_CLIENT_SYNC_INTERVAL = METHOD_GET_CLIENT_SYNC_INTERVAL + 1; + private static final int METHOD_GET_PANIC_THRESHOLD = METHOD_SET_CLIENT_SYNC_INTERVAL + 1; + private static final int METHOD_SET_PANIC_THRESHOLD = METHOD_GET_PANIC_THRESHOLD + 1; + private static final int METHOD_GET_AUTO_DISABLE = METHOD_SET_PANIC_THRESHOLD + 1; + private static final int METHOD_SET_AUTO_DISABLE = METHOD_GET_AUTO_DISABLE + 1; + private static final int METHOD_FORCE_NETWORKLESS_MASTER_MODE = METHOD_SET_AUTO_DISABLE + 1; +} diff --git a/src/main/java/android/os/CommonTimeUtils.java b/src/main/java/android/os/CommonTimeUtils.java new file mode 100644 index 0000000..ba060b8 --- /dev/null +++ b/src/main/java/android/os/CommonTimeUtils.java @@ -0,0 +1,293 @@ +/* + * 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 android.os; + +import java.net.InetAddress; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.util.Locale; +import static android.system.OsConstants.*; + +class CommonTimeUtils { + /** + * Successful operation. + */ + public static final int SUCCESS = 0; + /** + * Unspecified error. + */ + public static final int ERROR = -1; + /** + * Operation failed due to bad parameter value. + */ + public static final int ERROR_BAD_VALUE = -4; + /** + * Operation failed due to dead remote object. + */ + public static final int ERROR_DEAD_OBJECT = -7; + + public CommonTimeUtils(IBinder remote, String interfaceDesc) { + mRemote = remote; + mInterfaceDesc = interfaceDesc; + } + + public int transactGetInt(int method_code, int error_ret_val) + throws RemoteException { + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + int ret_val; + + try { + int res; + data.writeInterfaceToken(mInterfaceDesc); + mRemote.transact(method_code, data, reply, 0); + + res = reply.readInt(); + ret_val = (0 == res) ? reply.readInt() : error_ret_val; + } + finally { + reply.recycle(); + data.recycle(); + } + + return ret_val; + } + + public int transactSetInt(int method_code, int val) { + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + + try { + data.writeInterfaceToken(mInterfaceDesc); + data.writeInt(val); + mRemote.transact(method_code, data, reply, 0); + + return reply.readInt(); + } + catch (RemoteException e) { + return ERROR_DEAD_OBJECT; + } + finally { + reply.recycle(); + data.recycle(); + } + } + + public long transactGetLong(int method_code, long error_ret_val) + throws RemoteException { + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + long ret_val; + + try { + int res; + data.writeInterfaceToken(mInterfaceDesc); + mRemote.transact(method_code, data, reply, 0); + + res = reply.readInt(); + ret_val = (0 == res) ? reply.readLong() : error_ret_val; + } + finally { + reply.recycle(); + data.recycle(); + } + + return ret_val; + } + + public int transactSetLong(int method_code, long val) { + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + + try { + data.writeInterfaceToken(mInterfaceDesc); + data.writeLong(val); + mRemote.transact(method_code, data, reply, 0); + + return reply.readInt(); + } + catch (RemoteException e) { + return ERROR_DEAD_OBJECT; + } + finally { + reply.recycle(); + data.recycle(); + } + } + + public String transactGetString(int method_code, String error_ret_val) + throws RemoteException { + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + String ret_val; + + try { + int res; + data.writeInterfaceToken(mInterfaceDesc); + mRemote.transact(method_code, data, reply, 0); + + res = reply.readInt(); + ret_val = (0 == res) ? reply.readString() : error_ret_val; + } + finally { + reply.recycle(); + data.recycle(); + } + + return ret_val; + } + + public int transactSetString(int method_code, String val) { + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + + try { + data.writeInterfaceToken(mInterfaceDesc); + data.writeString(val); + mRemote.transact(method_code, data, reply, 0); + + return reply.readInt(); + } + catch (RemoteException e) { + return ERROR_DEAD_OBJECT; + } + finally { + reply.recycle(); + data.recycle(); + } + } + + public InetSocketAddress transactGetSockaddr(int method_code) + throws RemoteException { + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + InetSocketAddress ret_val = null; + + try { + int res; + data.writeInterfaceToken(mInterfaceDesc); + mRemote.transact(method_code, data, reply, 0); + + res = reply.readInt(); + if (0 == res) { + int type; + int port = 0; + String addrStr = null; + + type = reply.readInt(); + + if (AF_INET == type) { + int addr = reply.readInt(); + port = reply.readInt(); + addrStr = String.format(Locale.US, "%d.%d.%d.%d", + (addr >> 24) & 0xFF, + (addr >> 16) & 0xFF, + (addr >> 8) & 0xFF, + addr & 0xFF); + } else if (AF_INET6 == type) { + int addr1 = reply.readInt(); + int addr2 = reply.readInt(); + int addr3 = reply.readInt(); + int addr4 = reply.readInt(); + + port = reply.readInt(); + + int flowinfo = reply.readInt(); + int scope_id = reply.readInt(); + + addrStr = String.format(Locale.US, "[%04X:%04X:%04X:%04X:%04X:%04X:%04X:%04X]", + (addr1 >> 16) & 0xFFFF, addr1 & 0xFFFF, + (addr2 >> 16) & 0xFFFF, addr2 & 0xFFFF, + (addr3 >> 16) & 0xFFFF, addr3 & 0xFFFF, + (addr4 >> 16) & 0xFFFF, addr4 & 0xFFFF); + } + + if (null != addrStr) { + ret_val = new InetSocketAddress(addrStr, port); + } + } + } + finally { + reply.recycle(); + data.recycle(); + } + + return ret_val; + } + + public int transactSetSockaddr(int method_code, InetSocketAddress addr) { + android.os.Parcel data = android.os.Parcel.obtain(); + android.os.Parcel reply = android.os.Parcel.obtain(); + int ret_val = ERROR; + + try { + data.writeInterfaceToken(mInterfaceDesc); + + if (null == addr) { + data.writeInt(0); + } else { + data.writeInt(1); + final InetAddress a = addr.getAddress(); + final byte[] b = a.getAddress(); + final int p = addr.getPort(); + + if (a instanceof Inet4Address) { + int v4addr = (((int)b[0] & 0xFF) << 24) | + (((int)b[1] & 0xFF) << 16) | + (((int)b[2] & 0xFF) << 8) | + ((int)b[3] & 0xFF); + + data.writeInt(AF_INET); + data.writeInt(v4addr); + data.writeInt(p); + } else + if (a instanceof Inet6Address) { + int i; + Inet6Address v6 = (Inet6Address)a; + data.writeInt(AF_INET6); + for (i = 0; i < 4; ++i) { + int aword = (((int)b[(i*4) + 0] & 0xFF) << 24) | + (((int)b[(i*4) + 1] & 0xFF) << 16) | + (((int)b[(i*4) + 2] & 0xFF) << 8) | + ((int)b[(i*4) + 3] & 0xFF); + data.writeInt(aword); + } + data.writeInt(p); + data.writeInt(0); // flow info + data.writeInt(v6.getScopeId()); + } else { + return ERROR_BAD_VALUE; + } + } + + mRemote.transact(method_code, data, reply, 0); + ret_val = reply.readInt(); + } + catch (RemoteException e) { + ret_val = ERROR_DEAD_OBJECT; + } + finally { + reply.recycle(); + data.recycle(); + } + + return ret_val; + } + + private IBinder mRemote; + private String mInterfaceDesc; +}; diff --git a/src/main/java/android/os/ConditionVariable.java b/src/main/java/android/os/ConditionVariable.java new file mode 100644 index 0000000..1e820f9 --- /dev/null +++ b/src/main/java/android/os/ConditionVariable.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2006 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 android.os; + +/** + * Class that implements the condition variable locking paradigm. + * + *

+ * This differs from the built-in java.lang.Object wait() and notify() + * in that this class contains the condition to wait on itself. That means + * open(), close() and block() are sticky. If open() is called before block(), + * block() will not block, and instead return immediately. + * + *

+ * This class uses itself as the object to wait on, so if you wait() + * or notify() on a ConditionVariable, the results are undefined. + */ +public class ConditionVariable +{ + private volatile boolean mCondition; + + /** + * Create the ConditionVariable in the default closed state. + */ + public ConditionVariable() + { + mCondition = false; + } + + /** + * Create the ConditionVariable with the given state. + * + *

+ * Pass true for opened and false for closed. + */ + public ConditionVariable(boolean state) + { + mCondition = state; + } + + /** + * Open the condition, and release all threads that are blocked. + * + *

+ * Any threads that later approach block() will not block unless close() + * is called. + */ + public void open() + { + synchronized (this) { + boolean old = mCondition; + mCondition = true; + if (!old) { + this.notifyAll(); + } + } + } + + /** + * Reset the condition to the closed state. + * + *

+ * Any threads that call block() will block until someone calls open. + */ + public void close() + { + synchronized (this) { + mCondition = false; + } + } + + /** + * Block the current thread until the condition is opened. + * + *

+ * If the condition is already opened, return immediately. + */ + public void block() + { + synchronized (this) { + while (!mCondition) { + try { + this.wait(); + } + catch (InterruptedException e) { + } + } + } + } + + /** + * Block the current thread until the condition is opened or until + * timeout milliseconds have passed. + * + *

+ * If the condition is already opened, return immediately. + * + * @param timeout the maximum time to wait in milliseconds. + * + * @return true if the condition was opened, false if the call returns + * because of the timeout. + */ + public boolean block(long timeout) + { + // Object.wait(0) means wait forever, to mimic this, we just + // call the other block() method in that case. It simplifies + // this code for the common case. + if (timeout != 0) { + synchronized (this) { + long now = System.currentTimeMillis(); + long end = now + timeout; + while (!mCondition && now < end) { + try { + this.wait(end-now); + } + catch (InterruptedException e) { + } + now = System.currentTimeMillis(); + } + return mCondition; + } + } else { + this.block(); + return true; + } + } +} diff --git a/src/main/java/android/os/CountDownTimer.java b/src/main/java/android/os/CountDownTimer.java new file mode 100644 index 0000000..58acbcf --- /dev/null +++ b/src/main/java/android/os/CountDownTimer.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2008 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 android.os; + +/** + * Schedule a countdown until a time in the future, with + * regular notifications on intervals along the way. + * + * Example of showing a 30 second countdown in a text field: + * + *

+ * new CountDownTimer(30000, 1000) {
+ *
+ *     public void onTick(long millisUntilFinished) {
+ *         mTextField.setText("seconds remaining: " + millisUntilFinished / 1000);
+ *     }
+ *
+ *     public void onFinish() {
+ *         mTextField.setText("done!");
+ *     }
+ *  }.start();
+ * 
+ * + * The calls to {@link #onTick(long)} are synchronized to this object so that + * one call to {@link #onTick(long)} won't ever occur before the previous + * callback is complete. This is only relevant when the implementation of + * {@link #onTick(long)} takes an amount of time to execute that is significant + * compared to the countdown interval. + */ +public abstract class CountDownTimer { + + /** + * Millis since epoch when alarm should stop. + */ + private final long mMillisInFuture; + + /** + * The interval in millis that the user receives callbacks + */ + private final long mCountdownInterval; + + private long mStopTimeInFuture; + + /** + * boolean representing if the timer was cancelled + */ + private boolean mCancelled = false; + + /** + * @param millisInFuture The number of millis in the future from the call + * to {@link #start()} until the countdown is done and {@link #onFinish()} + * is called. + * @param countDownInterval The interval along the way to receive + * {@link #onTick(long)} callbacks. + */ + public CountDownTimer(long millisInFuture, long countDownInterval) { + mMillisInFuture = millisInFuture; + mCountdownInterval = countDownInterval; + } + + /** + * Cancel the countdown. + */ + public synchronized final void cancel() { + mCancelled = true; + mHandler.removeMessages(MSG); + } + + /** + * Start the countdown. + */ + public synchronized final CountDownTimer start() { + mCancelled = false; + if (mMillisInFuture <= 0) { + onFinish(); + return this; + } + mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture; + mHandler.sendMessage(mHandler.obtainMessage(MSG)); + return this; + } + + + /** + * Callback fired on regular interval. + * @param millisUntilFinished The amount of time until finished. + */ + public abstract void onTick(long millisUntilFinished); + + /** + * Callback fired when the time is up. + */ + public abstract void onFinish(); + + + private static final int MSG = 1; + + + // handles counting down + private Handler mHandler = new Handler() { + + @Override + public void handleMessage(Message msg) { + + synchronized (CountDownTimer.this) { + if (mCancelled) { + return; + } + + final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime(); + + if (millisLeft <= 0) { + onFinish(); + } else if (millisLeft < mCountdownInterval) { + // no tick, just delay until done + sendMessageDelayed(obtainMessage(MSG), millisLeft); + } else { + long lastTickStart = SystemClock.elapsedRealtime(); + onTick(millisLeft); + + // take into account user's onTick taking time to execute + long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime(); + + // special case: user's onTick took more than interval to + // complete, skip to next interval + while (delay < 0) delay += mCountdownInterval; + + sendMessageDelayed(obtainMessage(MSG), delay); + } + } + } + }; +} diff --git a/src/main/java/android/os/CursorAnchorInfoTest.java b/src/main/java/android/os/CursorAnchorInfoTest.java new file mode 100644 index 0000000..d4244ba --- /dev/null +++ b/src/main/java/android/os/CursorAnchorInfoTest.java @@ -0,0 +1,460 @@ +/* + * 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 android.os; + +import android.graphics.Matrix; +import android.graphics.RectF; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.text.TextUtils; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.CursorAnchorInfo.Builder; + +import java.util.Objects; + +import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; +import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; +import static android.view.inputmethod.CursorAnchorInfo.FLAG_IS_RTL; + +public class CursorAnchorInfoTest extends InstrumentationTestCase { + private static final RectF[] MANY_BOUNDS = new RectF[] { + new RectF(101.0f, 201.0f, 301.0f, 401.0f), + new RectF(102.0f, 202.0f, 302.0f, 402.0f), + new RectF(103.0f, 203.0f, 303.0f, 403.0f), + new RectF(104.0f, 204.0f, 304.0f, 404.0f), + new RectF(105.0f, 205.0f, 305.0f, 405.0f), + new RectF(106.0f, 206.0f, 306.0f, 406.0f), + new RectF(107.0f, 207.0f, 307.0f, 407.0f), + new RectF(108.0f, 208.0f, 308.0f, 408.0f), + new RectF(109.0f, 209.0f, 309.0f, 409.0f), + new RectF(110.0f, 210.0f, 310.0f, 410.0f), + new RectF(111.0f, 211.0f, 311.0f, 411.0f), + new RectF(112.0f, 212.0f, 312.0f, 412.0f), + new RectF(113.0f, 213.0f, 313.0f, 413.0f), + new RectF(114.0f, 214.0f, 314.0f, 414.0f), + new RectF(115.0f, 215.0f, 315.0f, 415.0f), + new RectF(116.0f, 216.0f, 316.0f, 416.0f), + new RectF(117.0f, 217.0f, 317.0f, 417.0f), + new RectF(118.0f, 218.0f, 318.0f, 418.0f), + new RectF(119.0f, 219.0f, 319.0f, 419.0f), + }; + private static final int[] MANY_FLAGS_ARRAY = new int[] { + FLAG_HAS_INVISIBLE_REGION, + FLAG_HAS_INVISIBLE_REGION | FLAG_HAS_VISIBLE_REGION, + FLAG_HAS_VISIBLE_REGION, + FLAG_HAS_VISIBLE_REGION, + FLAG_HAS_VISIBLE_REGION, + FLAG_HAS_VISIBLE_REGION, + FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL, + FLAG_HAS_INVISIBLE_REGION | FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL, + FLAG_HAS_INVISIBLE_REGION | FLAG_IS_RTL, + FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL, + FLAG_HAS_VISIBLE_REGION, + FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL, + FLAG_HAS_VISIBLE_REGION, + FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL, + FLAG_HAS_VISIBLE_REGION, + FLAG_HAS_VISIBLE_REGION | FLAG_IS_RTL, + FLAG_HAS_VISIBLE_REGION, + FLAG_HAS_INVISIBLE_REGION, + FLAG_HAS_INVISIBLE_REGION | FLAG_IS_RTL, + }; + + @SmallTest + public void testBuilder() throws Exception { + final int SELECTION_START = 30; + final int SELECTION_END = 40; + final int COMPOSING_TEXT_START = 32; + final String COMPOSING_TEXT = "test"; + final int INSERTION_MARKER_FLAGS = + FLAG_HAS_VISIBLE_REGION | FLAG_HAS_INVISIBLE_REGION | FLAG_IS_RTL; + final float INSERTION_MARKER_HORIZONTAL = 10.5f; + final float INSERTION_MARKER_TOP = 100.1f; + final float INSERTION_MARKER_BASELINE = 110.4f; + final float INSERTION_MARKER_BOTOM = 111.0f; + + Matrix TRANSFORM_MATRIX = new Matrix(Matrix.IDENTITY_MATRIX); + TRANSFORM_MATRIX.setScale(10.0f, 20.0f); + + final Builder builder = new Builder(); + builder.setSelectionRange(SELECTION_START, SELECTION_END) + .setComposingText(COMPOSING_TEXT_START, COMPOSING_TEXT) + .setInsertionMarkerLocation(INSERTION_MARKER_HORIZONTAL, INSERTION_MARKER_TOP, + INSERTION_MARKER_BASELINE, INSERTION_MARKER_BOTOM, INSERTION_MARKER_FLAGS) + .setMatrix(TRANSFORM_MATRIX); + for (int i = 0; i < MANY_BOUNDS.length; i++) { + final RectF bounds = MANY_BOUNDS[i]; + final int flags = MANY_FLAGS_ARRAY[i]; + builder.addCharacterBounds(i, bounds.left, bounds.top, bounds.right, bounds.bottom, + flags); + } + + final CursorAnchorInfo info = builder.build(); + assertEquals(SELECTION_START, info.getSelectionStart()); + assertEquals(SELECTION_END, info.getSelectionEnd()); + assertEquals(COMPOSING_TEXT_START, info.getComposingTextStart()); + assertTrue(TextUtils.equals(COMPOSING_TEXT, info.getComposingText())); + assertEquals(INSERTION_MARKER_FLAGS, info.getInsertionMarkerFlags()); + assertEquals(INSERTION_MARKER_HORIZONTAL, info.getInsertionMarkerHorizontal()); + assertEquals(INSERTION_MARKER_TOP, info.getInsertionMarkerTop()); + assertEquals(INSERTION_MARKER_BASELINE, info.getInsertionMarkerBaseline()); + assertEquals(INSERTION_MARKER_BOTOM, info.getInsertionMarkerBottom()); + assertEquals(TRANSFORM_MATRIX, info.getMatrix()); + for (int i = 0; i < MANY_BOUNDS.length; i++) { + final RectF expectedBounds = MANY_BOUNDS[i]; + assertEquals(expectedBounds, info.getCharacterBounds(i)); + } + assertNull(info.getCharacterBounds(-1)); + assertNull(info.getCharacterBounds(MANY_BOUNDS.length + 1)); + for (int i = 0; i < MANY_FLAGS_ARRAY.length; i++) { + final int expectedFlags = MANY_FLAGS_ARRAY[i]; + assertEquals(expectedFlags, info.getCharacterBoundsFlags(i)); + } + assertEquals(0, info.getCharacterBoundsFlags(-1)); + assertEquals(0, info.getCharacterBoundsFlags(MANY_BOUNDS.length + 1)); + + // Make sure that the builder can reproduce the same object. + final CursorAnchorInfo info2 = builder.build(); + assertEquals(SELECTION_START, info2.getSelectionStart()); + assertEquals(SELECTION_END, info2.getSelectionEnd()); + assertEquals(COMPOSING_TEXT_START, info2.getComposingTextStart()); + assertTrue(TextUtils.equals(COMPOSING_TEXT, info2.getComposingText())); + assertEquals(INSERTION_MARKER_FLAGS, info2.getInsertionMarkerFlags()); + assertEquals(INSERTION_MARKER_HORIZONTAL, info2.getInsertionMarkerHorizontal()); + assertEquals(INSERTION_MARKER_TOP, info2.getInsertionMarkerTop()); + assertEquals(INSERTION_MARKER_BASELINE, info2.getInsertionMarkerBaseline()); + assertEquals(INSERTION_MARKER_BOTOM, info2.getInsertionMarkerBottom()); + assertEquals(TRANSFORM_MATRIX, info2.getMatrix()); + for (int i = 0; i < MANY_BOUNDS.length; i++) { + final RectF expectedBounds = MANY_BOUNDS[i]; + assertEquals(expectedBounds, info2.getCharacterBounds(i)); + } + assertNull(info2.getCharacterBounds(-1)); + assertNull(info2.getCharacterBounds(MANY_BOUNDS.length + 1)); + for (int i = 0; i < MANY_FLAGS_ARRAY.length; i++) { + final int expectedFlags = MANY_FLAGS_ARRAY[i]; + assertEquals(expectedFlags, info2.getCharacterBoundsFlags(i)); + } + assertEquals(0, info2.getCharacterBoundsFlags(-1)); + assertEquals(0, info2.getCharacterBoundsFlags(MANY_BOUNDS.length + 1)); + assertEquals(info, info2); + assertEquals(info.hashCode(), info2.hashCode()); + + // Make sure that object can be marshaled via {@link Parsel}. + final CursorAnchorInfo info3 = cloneViaParcel(info2); + assertEquals(SELECTION_START, info3.getSelectionStart()); + assertEquals(SELECTION_END, info3.getSelectionEnd()); + assertEquals(COMPOSING_TEXT_START, info3.getComposingTextStart()); + assertTrue(TextUtils.equals(COMPOSING_TEXT, info3.getComposingText())); + assertEquals(INSERTION_MARKER_FLAGS, info3.getInsertionMarkerFlags()); + assertEquals(INSERTION_MARKER_HORIZONTAL, info3.getInsertionMarkerHorizontal()); + assertEquals(INSERTION_MARKER_TOP, info3.getInsertionMarkerTop()); + assertEquals(INSERTION_MARKER_BASELINE, info3.getInsertionMarkerBaseline()); + assertEquals(INSERTION_MARKER_BOTOM, info3.getInsertionMarkerBottom()); + assertEquals(TRANSFORM_MATRIX, info3.getMatrix()); + for (int i = 0; i < MANY_BOUNDS.length; i++) { + final RectF expectedBounds = MANY_BOUNDS[i]; + assertEquals(expectedBounds, info3.getCharacterBounds(i)); + } + assertNull(info3.getCharacterBounds(-1)); + assertNull(info3.getCharacterBounds(MANY_BOUNDS.length + 1)); + for (int i = 0; i < MANY_FLAGS_ARRAY.length; i++) { + final int expectedFlags = MANY_FLAGS_ARRAY[i]; + assertEquals(expectedFlags, info3.getCharacterBoundsFlags(i)); + } + assertEquals(0, info3.getCharacterBoundsFlags(-1)); + assertEquals(0, info3.getCharacterBoundsFlags(MANY_BOUNDS.length + 1)); + assertEquals(info.hashCode(), info3.hashCode()); + + builder.reset(); + final CursorAnchorInfo uninitializedInfo = builder.build(); + assertEquals(-1, uninitializedInfo.getSelectionStart()); + assertEquals(-1, uninitializedInfo.getSelectionEnd()); + assertEquals(-1, uninitializedInfo.getComposingTextStart()); + assertNull(uninitializedInfo.getComposingText()); + assertEquals(0, uninitializedInfo.getInsertionMarkerFlags()); + assertEquals(Float.NaN, uninitializedInfo.getInsertionMarkerHorizontal()); + assertEquals(Float.NaN, uninitializedInfo.getInsertionMarkerTop()); + assertEquals(Float.NaN, uninitializedInfo.getInsertionMarkerBaseline()); + assertEquals(Float.NaN, uninitializedInfo.getInsertionMarkerBottom()); + assertEquals(Matrix.IDENTITY_MATRIX, uninitializedInfo.getMatrix()); + } + + private static void assertNotEquals(final CursorAnchorInfo reference, + final CursorAnchorInfo actual) { + assertFalse(Objects.equals(reference, actual)); + } + + @SmallTest + public void testEquality() throws Exception { + final Matrix MATRIX1 = new Matrix(); + MATRIX1.setTranslate(10.0f, 20.0f); + final Matrix MATRIX2 = new Matrix(); + MATRIX2.setTranslate(110.0f, 120.0f); + final Matrix NAN_MATRIX = new Matrix(); + NAN_MATRIX.setValues(new float[]{ + Float.NaN, Float.NaN, Float.NaN, + Float.NaN, Float.NaN, Float.NaN, + Float.NaN, Float.NaN, Float.NaN}); + final int SELECTION_START1 = 2; + final int SELECTION_END1 = 7; + final String COMPOSING_TEXT1 = "0123456789"; + final int COMPOSING_TEXT_START1 = 0; + final int INSERTION_MARKER_FLAGS1 = FLAG_HAS_VISIBLE_REGION; + final float INSERTION_MARKER_HORIZONTAL1 = 10.5f; + final float INSERTION_MARKER_TOP1 = 100.1f; + final float INSERTION_MARKER_BASELINE1 = 110.4f; + final float INSERTION_MARKER_BOTOM1 = 111.0f; + final int SELECTION_START2 = 4; + final int SELECTION_END2 = 8; + final String COMPOSING_TEXT2 = "9876543210"; + final int COMPOSING_TEXT_START2 = 3; + final int INSERTION_MARKER_FLAGS2 = + FLAG_HAS_VISIBLE_REGION | FLAG_HAS_INVISIBLE_REGION | FLAG_IS_RTL; + final float INSERTION_MARKER_HORIZONTAL2 = 14.5f; + final float INSERTION_MARKER_TOP2 = 200.1f; + final float INSERTION_MARKER_BASELINE2 = 210.4f; + final float INSERTION_MARKER_BOTOM2 = 211.0f; + + // Default instance should be equal. + assertEquals(new Builder().build(), new Builder().build()); + + assertEquals( + new Builder().setSelectionRange(SELECTION_START1, SELECTION_END1).build(), + new Builder().setSelectionRange(SELECTION_START1, SELECTION_END1).build()); + assertNotEquals( + new Builder().setSelectionRange(SELECTION_START1, SELECTION_END1).build(), + new Builder().setSelectionRange(SELECTION_START1, SELECTION_END2).build()); + assertNotEquals( + new Builder().setSelectionRange(SELECTION_START1, SELECTION_END1).build(), + new Builder().setSelectionRange(SELECTION_START2, SELECTION_END1).build()); + assertNotEquals( + new Builder().setSelectionRange(SELECTION_START1, SELECTION_END1).build(), + new Builder().setSelectionRange(SELECTION_START2, SELECTION_END2).build()); + assertEquals( + new Builder().setComposingText(COMPOSING_TEXT_START1, COMPOSING_TEXT1).build(), + new Builder().setComposingText(COMPOSING_TEXT_START1, COMPOSING_TEXT1).build()); + assertNotEquals( + new Builder().setComposingText(COMPOSING_TEXT_START1, COMPOSING_TEXT1).build(), + new Builder().setComposingText(COMPOSING_TEXT_START2, COMPOSING_TEXT1).build()); + assertNotEquals( + new Builder().setComposingText(COMPOSING_TEXT_START1, COMPOSING_TEXT1).build(), + new Builder().setComposingText(COMPOSING_TEXT_START1, COMPOSING_TEXT2).build()); + assertNotEquals( + new Builder().setComposingText(COMPOSING_TEXT_START1, COMPOSING_TEXT1).build(), + new Builder().setComposingText(COMPOSING_TEXT_START2, COMPOSING_TEXT2).build()); + + // For insertion marker locations, {@link Float#NaN} is treated as if it was a number. + assertEquals( + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + Float.NaN, Float.NaN, Float.NaN, Float.NaN, + INSERTION_MARKER_FLAGS1).build(), + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + Float.NaN, Float.NaN, Float.NaN, Float.NaN, + INSERTION_MARKER_FLAGS1).build()); + + // Check Matrix. + assertEquals( + new Builder().setMatrix(MATRIX1).build(), + new Builder().setMatrix(MATRIX1).build()); + assertNotEquals( + new Builder().setMatrix(MATRIX1).build(), + new Builder().setMatrix(MATRIX2).build()); + assertNotEquals( + new Builder().setMatrix(MATRIX1).build(), + new Builder().setMatrix(NAN_MATRIX).build()); + // Unlike insertion marker locations, {@link Float#NaN} in the matrix is treated as just a + // NaN as usual (NaN == NaN -> false). + assertNotEquals( + new Builder().setMatrix(NAN_MATRIX).build(), + new Builder().setMatrix(NAN_MATRIX).build()); + + assertEquals( + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build(), + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build()); + assertNotEquals( + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + Float.NaN, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build(), + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build()); + assertNotEquals( + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build(), + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL2, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build()); + assertNotEquals( + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build(), + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP2, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build()); + assertNotEquals( + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build(), + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE2, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build()); + assertNotEquals( + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build(), + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL2, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build()); + assertNotEquals( + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build(), + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM2, + INSERTION_MARKER_FLAGS1).build()); + assertNotEquals( + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS1).build(), + new Builder().setMatrix(MATRIX1).setInsertionMarkerLocation( + INSERTION_MARKER_HORIZONTAL1, INSERTION_MARKER_TOP1, + INSERTION_MARKER_BASELINE1, INSERTION_MARKER_BOTOM1, + INSERTION_MARKER_FLAGS2).build()); + } + + @SmallTest + public void testMatrixIsCopied() throws Exception { + final Matrix MATRIX1 = new Matrix(); + MATRIX1.setTranslate(10.0f, 20.0f); + final Matrix MATRIX2 = new Matrix(); + MATRIX2.setTranslate(110.0f, 120.0f); + final Matrix MATRIX3 = new Matrix(); + MATRIX3.setTranslate(210.0f, 220.0f); + final Matrix matrix = new Matrix(); + final Builder builder = new Builder(); + + matrix.set(MATRIX1); + builder.setMatrix(matrix); + matrix.postRotate(90.0f); + + final CursorAnchorInfo firstInstance = builder.build(); + assertEquals(MATRIX1, firstInstance.getMatrix()); + matrix.set(MATRIX2); + builder.setMatrix(matrix); + final CursorAnchorInfo secondInstance = builder.build(); + assertEquals(MATRIX1, firstInstance.getMatrix()); + assertEquals(MATRIX2, secondInstance.getMatrix()); + + matrix.set(MATRIX3); + assertEquals(MATRIX1, firstInstance.getMatrix()); + assertEquals(MATRIX2, secondInstance.getMatrix()); + } + + @SmallTest + public void testMatrixIsRequired() throws Exception { + final int SELECTION_START = 30; + final int SELECTION_END = 40; + final int COMPOSING_TEXT_START = 32; + final String COMPOSING_TEXT = "test"; + final int INSERTION_MARKER_FLAGS = FLAG_HAS_VISIBLE_REGION; + final float INSERTION_MARKER_HORIZONTAL = 10.5f; + final float INSERTION_MARKER_TOP = 100.1f; + final float INSERTION_MARKER_BASELINE = 110.4f; + final float INSERTION_MARKER_BOTOM = 111.0f; + Matrix TRANSFORM_MATRIX = new Matrix(Matrix.IDENTITY_MATRIX); + TRANSFORM_MATRIX.setScale(10.0f, 20.0f); + + final Builder builder = new Builder(); + // Check twice to make sure if Builder#reset() works as expected. + for (int repeatCount = 0; repeatCount < 2; ++repeatCount) { + builder.setSelectionRange(SELECTION_START, SELECTION_END) + .setComposingText(COMPOSING_TEXT_START, COMPOSING_TEXT); + try { + // Should succeed as coordinate transformation matrix is not required if no + // positional information is specified. + builder.build(); + } catch (IllegalArgumentException ex) { + assertTrue(false); + } + + builder.setInsertionMarkerLocation(INSERTION_MARKER_HORIZONTAL, INSERTION_MARKER_TOP, + INSERTION_MARKER_BASELINE, INSERTION_MARKER_BOTOM, INSERTION_MARKER_FLAGS); + try { + // Coordinate transformation matrix is required if no positional information is + // specified. + builder.build(); + assertTrue(false); + } catch (IllegalArgumentException ex) { + } + + builder.setMatrix(TRANSFORM_MATRIX); + try { + // Should succeed as coordinate transformation matrix is required. + builder.build(); + } catch (IllegalArgumentException ex) { + assertTrue(false); + } + + builder.reset(); + } + } + + @SmallTest + public void testBuilderAddCharacterBounds() throws Exception { + // A negative index should be rejected. + try { + new Builder().addCharacterBounds(-1, 0.0f, 0.0f, 0.0f, 0.0f, FLAG_HAS_VISIBLE_REGION); + assertTrue(false); + } catch (IllegalArgumentException ex) { + } + } + + private static CursorAnchorInfo cloneViaParcel(final CursorAnchorInfo src) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + src.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return new CursorAnchorInfo(parcel); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } +} diff --git a/src/main/java/android/os/DeadObjectException.java b/src/main/java/android/os/DeadObjectException.java new file mode 100644 index 0000000..94c5387 --- /dev/null +++ b/src/main/java/android/os/DeadObjectException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2006 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 android.os; +import android.os.RemoteException; + +/** + * The object you are calling has died, because its hosting process + * no longer exists. + */ +public class DeadObjectException extends RemoteException { + public DeadObjectException() { + super(); + } +} diff --git a/src/main/java/android/os/Debug.java b/src/main/java/android/os/Debug.java new file mode 100644 index 0000000..b8178b4 --- /dev/null +++ b/src/main/java/android/os/Debug.java @@ -0,0 +1,1691 @@ +/* + * Copyright (C) 2007 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 android.os; + +import com.android.internal.util.FastPrintWriter; +import com.android.internal.util.TypedProperties; + +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.apache.harmony.dalvik.ddmc.Chunk; +import org.apache.harmony.dalvik.ddmc.ChunkHandler; +import org.apache.harmony.dalvik.ddmc.DdmServer; + +import dalvik.bytecode.OpcodeInfo; +import dalvik.system.VMDebug; + + +/** + * Provides various debugging methods for Android applications, including + * tracing and allocation counts. + *

Logging Trace Files

+ *

Debug can create log files that give details about an application, such as + * a call stack and start/stop times for any running methods. See Traceview: A Graphical Log Viewer for + * information about reading trace files. To start logging trace files, call one + * of the startMethodTracing() methods. To stop tracing, call + * {@link #stopMethodTracing()}. + */ +public final class Debug +{ + private static final String TAG = "Debug"; + + /** + * Flags for startMethodTracing(). These can be ORed together. + * + * TRACE_COUNT_ALLOCS adds the results from startAllocCounting to the + * trace key file. + */ + public static final int TRACE_COUNT_ALLOCS = VMDebug.TRACE_COUNT_ALLOCS; + + /** + * Flags for printLoadedClasses(). Default behavior is to only show + * the class name. + */ + public static final int SHOW_FULL_DETAIL = 1; + public static final int SHOW_CLASSLOADER = (1 << 1); + public static final int SHOW_INITIALIZED = (1 << 2); + + // set/cleared by waitForDebugger() + private static volatile boolean mWaiting = false; + + private Debug() {} + + /* + * How long to wait for the debugger to finish sending requests. I've + * seen this hit 800msec on the device while waiting for a response + * to travel over USB and get processed, so we take that and add + * half a second. + */ + private static final int MIN_DEBUGGER_IDLE = 1300; // msec + + /* how long to sleep when polling for activity */ + private static final int SPIN_DELAY = 200; // msec + + /** + * Default trace file path and file + */ + private static final String DEFAULT_TRACE_PATH_PREFIX = + Environment.getLegacyExternalStorageDirectory().getPath() + "/"; + private static final String DEFAULT_TRACE_BODY = "dmtrace"; + private static final String DEFAULT_TRACE_EXTENSION = ".trace"; + private static final String DEFAULT_TRACE_FILE_PATH = + DEFAULT_TRACE_PATH_PREFIX + DEFAULT_TRACE_BODY + + DEFAULT_TRACE_EXTENSION; + + + /** + * This class is used to retrieved various statistics about the memory mappings for this + * process. The returns info broken down by dalvik, native, and other. All results are in kB. + */ + public static class MemoryInfo implements Parcelable { + /** The proportional set size for dalvik heap. (Doesn't include other Dalvik overhead.) */ + public int dalvikPss; + /** The proportional set size that is swappable for dalvik heap. */ + /** @hide We may want to expose this, eventually. */ + public int dalvikSwappablePss; + /** The private dirty pages used by dalvik heap. */ + public int dalvikPrivateDirty; + /** The shared dirty pages used by dalvik heap. */ + public int dalvikSharedDirty; + /** The private clean pages used by dalvik heap. */ + /** @hide We may want to expose this, eventually. */ + public int dalvikPrivateClean; + /** The shared clean pages used by dalvik heap. */ + /** @hide We may want to expose this, eventually. */ + public int dalvikSharedClean; + /** The dirty dalvik pages that have been swapped out. */ + /** @hide We may want to expose this, eventually. */ + public int dalvikSwappedOut; + + /** The proportional set size for the native heap. */ + public int nativePss; + /** The proportional set size that is swappable for the native heap. */ + /** @hide We may want to expose this, eventually. */ + public int nativeSwappablePss; + /** The private dirty pages used by the native heap. */ + public int nativePrivateDirty; + /** The shared dirty pages used by the native heap. */ + public int nativeSharedDirty; + /** The private clean pages used by the native heap. */ + /** @hide We may want to expose this, eventually. */ + public int nativePrivateClean; + /** The shared clean pages used by the native heap. */ + /** @hide We may want to expose this, eventually. */ + public int nativeSharedClean; + /** The dirty native pages that have been swapped out. */ + /** @hide We may want to expose this, eventually. */ + public int nativeSwappedOut; + + /** The proportional set size for everything else. */ + public int otherPss; + /** The proportional set size that is swappable for everything else. */ + /** @hide We may want to expose this, eventually. */ + public int otherSwappablePss; + /** The private dirty pages used by everything else. */ + public int otherPrivateDirty; + /** The shared dirty pages used by everything else. */ + public int otherSharedDirty; + /** The private clean pages used by everything else. */ + /** @hide We may want to expose this, eventually. */ + public int otherPrivateClean; + /** The shared clean pages used by everything else. */ + /** @hide We may want to expose this, eventually. */ + public int otherSharedClean; + /** The dirty pages used by anyting else that have been swapped out. */ + /** @hide We may want to expose this, eventually. */ + public int otherSwappedOut; + + /** @hide */ + public static final int NUM_OTHER_STATS = 17; + + /** @hide */ + public static final int NUM_DVK_STATS = 8; + + /** @hide */ + public static final int NUM_CATEGORIES = 7; + + /** @hide */ + public static final int offsetPss = 0; + /** @hide */ + public static final int offsetSwappablePss = 1; + /** @hide */ + public static final int offsetPrivateDirty = 2; + /** @hide */ + public static final int offsetSharedDirty = 3; + /** @hide */ + public static final int offsetPrivateClean = 4; + /** @hide */ + public static final int offsetSharedClean = 5; + /** @hide */ + public static final int offsetSwappedOut = 6; + + private int[] otherStats = new int[(NUM_OTHER_STATS+NUM_DVK_STATS)*NUM_CATEGORIES]; + + public MemoryInfo() { + } + + /** + * Return total PSS memory usage in kB. + */ + public int getTotalPss() { + return dalvikPss + nativePss + otherPss; + } + + /** + * @hide Return total PSS memory usage in kB. + */ + public int getTotalUss() { + return dalvikPrivateClean + dalvikPrivateDirty + + nativePrivateClean + nativePrivateDirty + + otherPrivateClean + otherPrivateDirty; + } + + /** + * Return total PSS memory usage in kB. + */ + public int getTotalSwappablePss() { + return dalvikSwappablePss + nativeSwappablePss + otherSwappablePss; + } + + /** + * Return total private dirty memory usage in kB. + */ + public int getTotalPrivateDirty() { + return dalvikPrivateDirty + nativePrivateDirty + otherPrivateDirty; + } + + /** + * Return total shared dirty memory usage in kB. + */ + public int getTotalSharedDirty() { + return dalvikSharedDirty + nativeSharedDirty + otherSharedDirty; + } + + /** + * Return total shared clean memory usage in kB. + */ + public int getTotalPrivateClean() { + return dalvikPrivateClean + nativePrivateClean + otherPrivateClean; + } + + /** + * Return total shared clean memory usage in kB. + */ + public int getTotalSharedClean() { + return dalvikSharedClean + nativeSharedClean + otherSharedClean; + } + + /** + * Return total swapped out memory in kB. + * @hide + */ + public int getTotalSwappedOut() { + return dalvikSwappedOut + nativeSwappedOut + otherSwappedOut; + } + + /** @hide */ + public int getOtherPss(int which) { + return otherStats[which*NUM_CATEGORIES + offsetPss]; + } + + + /** @hide */ + public int getOtherSwappablePss(int which) { + return otherStats[which*NUM_CATEGORIES + offsetSwappablePss]; + } + + + /** @hide */ + public int getOtherPrivateDirty(int which) { + return otherStats[which*NUM_CATEGORIES + offsetPrivateDirty]; + } + + /** @hide */ + public int getOtherSharedDirty(int which) { + return otherStats[which*NUM_CATEGORIES + offsetSharedDirty]; + } + + /** @hide */ + public int getOtherPrivateClean(int which) { + return otherStats[which*NUM_CATEGORIES + offsetPrivateClean]; + } + + /** @hide */ + public int getOtherSharedClean(int which) { + return otherStats[which*NUM_CATEGORIES + offsetSharedClean]; + } + + /** @hide */ + public int getOtherSwappedOut(int which) { + return otherStats[which*NUM_CATEGORIES + offsetSwappedOut]; + } + + /** @hide */ + public static String getOtherLabel(int which) { + switch (which) { + case 0: return "Dalvik Other"; + case 1: return "Stack"; + case 2: return "Cursor"; + case 3: return "Ashmem"; + case 4: return "Gfx dev"; + case 5: return "Other dev"; + case 6: return ".so mmap"; + case 7: return ".jar mmap"; + case 8: return ".apk mmap"; + case 9: return ".ttf mmap"; + case 10: return ".dex mmap"; + case 11: return ".oat mmap"; + case 12: return ".art mmap"; + case 13: return "Other mmap"; + case 14: return "EGL mtrack"; + case 15: return "GL mtrack"; + case 16: return "Other mtrack"; + case 17: return ".Heap"; + case 18: return ".LOS"; + case 19: return ".LinearAlloc"; + case 20: return ".GC"; + case 21: return ".JITCache"; + case 22: return ".Zygote"; + case 23: return ".NonMoving"; + case 24: return ".IndirectRef"; + default: return "????"; + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(dalvikPss); + dest.writeInt(dalvikSwappablePss); + dest.writeInt(dalvikPrivateDirty); + dest.writeInt(dalvikSharedDirty); + dest.writeInt(dalvikPrivateClean); + dest.writeInt(dalvikSharedClean); + dest.writeInt(dalvikSwappedOut); + dest.writeInt(nativePss); + dest.writeInt(nativeSwappablePss); + dest.writeInt(nativePrivateDirty); + dest.writeInt(nativeSharedDirty); + dest.writeInt(nativePrivateClean); + dest.writeInt(nativeSharedClean); + dest.writeInt(nativeSwappedOut); + dest.writeInt(otherPss); + dest.writeInt(otherSwappablePss); + dest.writeInt(otherPrivateDirty); + dest.writeInt(otherSharedDirty); + dest.writeInt(otherPrivateClean); + dest.writeInt(otherSharedClean); + dest.writeInt(otherSwappedOut); + dest.writeIntArray(otherStats); + } + + public void readFromParcel(Parcel source) { + dalvikPss = source.readInt(); + dalvikSwappablePss = source.readInt(); + dalvikPrivateDirty = source.readInt(); + dalvikSharedDirty = source.readInt(); + dalvikPrivateClean = source.readInt(); + dalvikSharedClean = source.readInt(); + dalvikSwappedOut = source.readInt(); + nativePss = source.readInt(); + nativeSwappablePss = source.readInt(); + nativePrivateDirty = source.readInt(); + nativeSharedDirty = source.readInt(); + nativePrivateClean = source.readInt(); + nativeSharedClean = source.readInt(); + nativeSwappedOut = source.readInt(); + otherPss = source.readInt(); + otherSwappablePss = source.readInt(); + otherPrivateDirty = source.readInt(); + otherSharedDirty = source.readInt(); + otherPrivateClean = source.readInt(); + otherSharedClean = source.readInt(); + otherSwappedOut = source.readInt(); + otherStats = source.createIntArray(); + } + + public static final Creator CREATOR = new Creator() { + public MemoryInfo createFromParcel(Parcel source) { + return new MemoryInfo(source); + } + public MemoryInfo[] newArray(int size) { + return new MemoryInfo[size]; + } + }; + + private MemoryInfo(Parcel source) { + readFromParcel(source); + } + } + + + /** + * Wait until a debugger attaches. As soon as the debugger attaches, + * this returns, so you will need to place a breakpoint after the + * waitForDebugger() call if you want to start tracing immediately. + */ + public static void waitForDebugger() { + if (!VMDebug.isDebuggingEnabled()) { + //System.out.println("debugging not enabled, not waiting"); + return; + } + if (isDebuggerConnected()) + return; + + // if DDMS is listening, inform them of our plight + System.out.println("Sending WAIT chunk"); + byte[] data = new byte[] { 0 }; // 0 == "waiting for debugger" + Chunk waitChunk = new Chunk(ChunkHandler.type("WAIT"), data, 0, 1); + DdmServer.sendChunk(waitChunk); + + mWaiting = true; + while (!isDebuggerConnected()) { + try { Thread.sleep(SPIN_DELAY); } + catch (InterruptedException ie) {} + } + mWaiting = false; + + System.out.println("Debugger has connected"); + + /* + * There is no "ready to go" signal from the debugger, and we're + * not allowed to suspend ourselves -- the debugger expects us to + * be running happily, and gets confused if we aren't. We need to + * allow the debugger a chance to set breakpoints before we start + * running again. + * + * Sit and spin until the debugger has been idle for a short while. + */ + while (true) { + long delta = VMDebug.lastDebuggerActivity(); + if (delta < 0) { + System.out.println("debugger detached?"); + break; + } + + if (delta < MIN_DEBUGGER_IDLE) { + System.out.println("waiting for debugger to settle..."); + try { Thread.sleep(SPIN_DELAY); } + catch (InterruptedException ie) {} + } else { + System.out.println("debugger has settled (" + delta + ")"); + break; + } + } + } + + /** + * Returns "true" if one or more threads is waiting for a debugger + * to attach. + */ + public static boolean waitingForDebugger() { + return mWaiting; + } + + /** + * Determine if a debugger is currently attached. + */ + public static boolean isDebuggerConnected() { + return VMDebug.isDebuggerConnected(); + } + + /** + * Returns an array of strings that identify VM features. This is + * used by DDMS to determine what sorts of operations the VM can + * perform. + * + * @hide + */ + public static String[] getVmFeatureList() { + return VMDebug.getVmFeatureList(); + } + + /** + * Change the JDWP port. + * + * @deprecated no longer needed or useful + */ + @Deprecated + public static void changeDebugPort(int port) {} + + /** + * This is the pathname to the sysfs file that enables and disables + * tracing on the qemu emulator. + */ + private static final String SYSFS_QEMU_TRACE_STATE = "/sys/qemu_trace/state"; + + /** + * Enable qemu tracing. For this to work requires running everything inside + * the qemu emulator; otherwise, this method will have no effect. The trace + * file is specified on the command line when the emulator is started. For + * example, the following command line
+ * emulator -trace foo
+ * will start running the emulator and create a trace file named "foo". This + * method simply enables writing the trace records to the trace file. + * + *

+ * The main differences between this and {@link #startMethodTracing()} are + * that tracing in the qemu emulator traces every cpu instruction of every + * process, including kernel code, so we have more complete information, + * including all context switches. We can also get more detailed information + * such as cache misses. The sequence of calls is determined by + * post-processing the instruction trace. The qemu tracing is also done + * without modifying the application or perturbing the timing of calls + * because no instrumentation is added to the application being traced. + *

+ * + *

+ * One limitation of using this method compared to using + * {@link #startMethodTracing()} on the real device is that the emulator + * does not model all of the real hardware effects such as memory and + * bus contention. The emulator also has a simple cache model and cannot + * capture all the complexities of a real cache. + *

+ */ + public static void startNativeTracing() { + // Open the sysfs file for writing and write "1" to it. + PrintWriter outStream = null; + try { + FileOutputStream fos = new FileOutputStream(SYSFS_QEMU_TRACE_STATE); + outStream = new FastPrintWriter(fos); + outStream.println("1"); + } catch (Exception e) { + } finally { + if (outStream != null) + outStream.close(); + } + + VMDebug.startEmulatorTracing(); + } + + /** + * Stop qemu tracing. See {@link #startNativeTracing()} to start tracing. + * + *

Tracing can be started and stopped as many times as desired. When + * the qemu emulator itself is stopped then the buffered trace records + * are flushed and written to the trace file. In fact, it is not necessary + * to call this method at all; simply killing qemu is sufficient. But + * starting and stopping a trace is useful for examining a specific + * region of code.

+ */ + public static void stopNativeTracing() { + VMDebug.stopEmulatorTracing(); + + // Open the sysfs file for writing and write "0" to it. + PrintWriter outStream = null; + try { + FileOutputStream fos = new FileOutputStream(SYSFS_QEMU_TRACE_STATE); + outStream = new FastPrintWriter(fos); + outStream.println("0"); + } catch (Exception e) { + // We could print an error message here but we probably want + // to quietly ignore errors if we are not running in the emulator. + } finally { + if (outStream != null) + outStream.close(); + } + } + + /** + * Enable "emulator traces", in which information about the current + * method is made available to the "emulator -trace" feature. There + * is no corresponding "disable" call -- this is intended for use by + * the framework when tracing should be turned on and left that way, so + * that traces captured with F9/F10 will include the necessary data. + * + * This puts the VM into "profile" mode, which has performance + * consequences. + * + * To temporarily enable tracing, use {@link #startNativeTracing()}. + */ + public static void enableEmulatorTraceOutput() { + VMDebug.startEmulatorTracing(); + } + + /** + * Start method tracing with default log name and buffer size. See Traceview: A Graphical Log Viewer for + * information about reading these files. Call stopMethodTracing() to stop + * tracing. + */ + public static void startMethodTracing() { + VMDebug.startMethodTracing(DEFAULT_TRACE_FILE_PATH, 0, 0, false, 0); + } + + /** + * Start method tracing, specifying the trace log file name. The trace + * file will be put under "/sdcard" unless an absolute path is given. + * See Traceview: A Graphical Log Viewer for + * information about reading trace files. + * + * @param traceName Name for the trace log file to create. + * If {@code traceName} is null, this value defaults to "/sdcard/dmtrace.trace". + * If the files already exist, they will be truncated. + * If the trace file given does not end in ".trace", it will be appended for you. + */ + public static void startMethodTracing(String traceName) { + startMethodTracing(traceName, 0, 0); + } + + /** + * Start method tracing, specifying the trace log file name and the + * buffer size. The trace files will be put under "/sdcard" unless an + * absolute path is given. See Traceview: A Graphical Log Viewer for + * information about reading trace files. + * @param traceName Name for the trace log file to create. + * If {@code traceName} is null, this value defaults to "/sdcard/dmtrace.trace". + * If the files already exist, they will be truncated. + * If the trace file given does not end in ".trace", it will be appended for you. + * + * @param bufferSize The maximum amount of trace data we gather. If not given, it defaults to 8MB. + */ + public static void startMethodTracing(String traceName, int bufferSize) { + startMethodTracing(traceName, bufferSize, 0); + } + + /** + * Start method tracing, specifying the trace log file name and the + * buffer size. The trace files will be put under "/sdcard" unless an + * absolute path is given. See Traceview: A Graphical Log Viewer for + * information about reading trace files. + * + *

+ * When method tracing is enabled, the VM will run more slowly than + * usual, so the timings from the trace files should only be considered + * in relative terms (e.g. was run #1 faster than run #2). The times + * for native methods will not change, so don't try to use this to + * compare the performance of interpreted and native implementations of the + * same method. As an alternative, consider using sampling-based method + * tracing via {@link #startMethodTracingSampling(String, int, int)} or + * "native" tracing in the emulator via {@link #startNativeTracing()}. + *

+ * + * @param traceName Name for the trace log file to create. + * If {@code traceName} is null, this value defaults to "/sdcard/dmtrace.trace". + * If the files already exist, they will be truncated. + * If the trace file given does not end in ".trace", it will be appended for you. + * @param bufferSize The maximum amount of trace data we gather. If not given, it defaults to 8MB. + * @param flags Flags to control method tracing. The only one that is currently defined is {@link #TRACE_COUNT_ALLOCS}. + */ + public static void startMethodTracing(String traceName, int bufferSize, + int flags) { + VMDebug.startMethodTracing(fixTraceName(traceName), bufferSize, flags, false, 0); + } + + /** + * Start sampling-based method tracing, specifying the trace log file name, + * the buffer size, and the sampling interval. The trace files will be put + * under "/sdcard" unless an absolute path is given. See Traceview: A Graphical Log Viewer + * for information about reading trace files. + * + * @param traceName Name for the trace log file to create. + * If {@code traceName} is null, this value defaults to "/sdcard/dmtrace.trace". + * If the files already exist, they will be truncated. + * If the trace file given does not end in ".trace", it will be appended for you. + * @param bufferSize The maximum amount of trace data we gather. If not given, it defaults to 8MB. + * @param intervalUs The amount of time between each sample in microseconds. + */ + public static void startMethodTracingSampling(String traceName, + int bufferSize, int intervalUs) { + VMDebug.startMethodTracing(fixTraceName(traceName), bufferSize, 0, true, intervalUs); + } + + /** + * Formats name of trace log file for method tracing. + */ + private static String fixTraceName(String traceName) { + if (traceName == null) + traceName = DEFAULT_TRACE_FILE_PATH; + if (traceName.charAt(0) != '/') + traceName = DEFAULT_TRACE_PATH_PREFIX + traceName; + if (!traceName.endsWith(DEFAULT_TRACE_EXTENSION)) + traceName = traceName + DEFAULT_TRACE_EXTENSION; + + return traceName; + } + + /** + * Like startMethodTracing(String, int, int), but taking an already-opened + * FileDescriptor in which the trace is written. The file name is also + * supplied simply for logging. Makes a dup of the file descriptor. + * + * Not exposed in the SDK unless we are really comfortable with supporting + * this and find it would be useful. + * @hide + */ + public static void startMethodTracing(String traceName, FileDescriptor fd, + int bufferSize, int flags) { + VMDebug.startMethodTracing(traceName, fd, bufferSize, flags, false, 0); + } + + /** + * Starts method tracing without a backing file. When stopMethodTracing + * is called, the result is sent directly to DDMS. (If DDMS is not + * attached when tracing ends, the profiling data will be discarded.) + * + * @hide + */ + public static void startMethodTracingDdms(int bufferSize, int flags, + boolean samplingEnabled, int intervalUs) { + VMDebug.startMethodTracingDdms(bufferSize, flags, samplingEnabled, intervalUs); + } + + /** + * Determine whether method tracing is currently active and what type is + * active. + * + * @hide + */ + public static int getMethodTracingMode() { + return VMDebug.getMethodTracingMode(); + } + + /** + * Stop method tracing. + */ + public static void stopMethodTracing() { + VMDebug.stopMethodTracing(); + } + + /** + * Get an indication of thread CPU usage. The value returned + * indicates the amount of time that the current thread has spent + * executing code or waiting for certain types of I/O. + * + * The time is expressed in nanoseconds, and is only meaningful + * when compared to the result from an earlier call. Note that + * nanosecond resolution does not imply nanosecond accuracy. + * + * On system which don't support this operation, the call returns -1. + */ + public static long threadCpuTimeNanos() { + return VMDebug.threadCpuTimeNanos(); + } + + /** + * Start counting the number and aggregate size of memory allocations. + * + *

The {@link #startAllocCounting() start} method resets the counts and enables counting. + * The {@link #stopAllocCounting() stop} method disables the counting so that the analysis + * code doesn't cause additional allocations. The various get methods return + * the specified value. And the various reset methods reset the specified + * count.

+ * + *

Counts are kept for the system as a whole (global) and for each thread. + * The per-thread counts for threads other than the current thread + * are not cleared by the "reset" or "start" calls.

+ * + * @deprecated Accurate counting is a burden on the runtime and may be removed. + */ + @Deprecated + public static void startAllocCounting() { + VMDebug.startAllocCounting(); + } + + /** + * Stop counting the number and aggregate size of memory allocations. + * + * @see #startAllocCounting() + */ + @Deprecated + public static void stopAllocCounting() { + VMDebug.stopAllocCounting(); + } + + /** + * Returns the global count of objects allocated by the runtime between a + * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + */ + public static int getGlobalAllocCount() { + return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_OBJECTS); + } + + /** + * Clears the global count of objects allocated. + * @see #getGlobalAllocCount() + */ + public static void resetGlobalAllocCount() { + VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_OBJECTS); + } + + /** + * Returns the global size, in bytes, of objects allocated by the runtime between a + * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + */ + public static int getGlobalAllocSize() { + return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_BYTES); + } + + /** + * Clears the global size of objects allocated. + * @see #getGlobalAllocSize() + */ + public static void resetGlobalAllocSize() { + VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_ALLOCATED_BYTES); + } + + /** + * Returns the global count of objects freed by the runtime between a + * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + */ + public static int getGlobalFreedCount() { + return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_FREED_OBJECTS); + } + + /** + * Clears the global count of objects freed. + * @see #getGlobalFreedCount() + */ + public static void resetGlobalFreedCount() { + VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_FREED_OBJECTS); + } + + /** + * Returns the global size, in bytes, of objects freed by the runtime between a + * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + */ + public static int getGlobalFreedSize() { + return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_FREED_BYTES); + } + + /** + * Clears the global size of objects freed. + * @see #getGlobalFreedSize() + */ + public static void resetGlobalFreedSize() { + VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_FREED_BYTES); + } + + /** + * Returns the number of non-concurrent GC invocations between a + * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + */ + public static int getGlobalGcInvocationCount() { + return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_GC_INVOCATIONS); + } + + /** + * Clears the count of non-concurrent GC invocations. + * @see #getGlobalGcInvocationCount() + */ + public static void resetGlobalGcInvocationCount() { + VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_GC_INVOCATIONS); + } + + /** + * Returns the number of classes successfully initialized (ie those that executed without + * throwing an exception) between a {@link #startAllocCounting() start} and + * {@link #stopAllocCounting() stop}. + */ + public static int getGlobalClassInitCount() { + return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_CLASS_INIT_COUNT); + } + + /** + * Clears the count of classes initialized. + * @see #getGlobalClassInitCount() + */ + public static void resetGlobalClassInitCount() { + VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_CLASS_INIT_COUNT); + } + + /** + * Returns the time spent successfully initializing classes between a + * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + */ + public static int getGlobalClassInitTime() { + /* cumulative elapsed time for class initialization, in usec */ + return VMDebug.getAllocCount(VMDebug.KIND_GLOBAL_CLASS_INIT_TIME); + } + + /** + * Clears the count of time spent initializing classes. + * @see #getGlobalClassInitTime() + */ + public static void resetGlobalClassInitTime() { + VMDebug.resetAllocCount(VMDebug.KIND_GLOBAL_CLASS_INIT_TIME); + } + + /** + * This method exists for compatibility and always returns 0. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static int getGlobalExternalAllocCount() { + return 0; + } + + /** + * This method exists for compatibility and has no effect. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static void resetGlobalExternalAllocSize() {} + + /** + * This method exists for compatibility and has no effect. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static void resetGlobalExternalAllocCount() {} + + /** + * This method exists for compatibility and always returns 0. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static int getGlobalExternalAllocSize() { + return 0; + } + + /** + * This method exists for compatibility and always returns 0. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static int getGlobalExternalFreedCount() { + return 0; + } + + /** + * This method exists for compatibility and has no effect. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static void resetGlobalExternalFreedCount() {} + + /** + * This method exists for compatibility and has no effect. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static int getGlobalExternalFreedSize() { + return 0; + } + + /** + * This method exists for compatibility and has no effect. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static void resetGlobalExternalFreedSize() {} + + /** + * Returns the thread-local count of objects allocated by the runtime between a + * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + */ + public static int getThreadAllocCount() { + return VMDebug.getAllocCount(VMDebug.KIND_THREAD_ALLOCATED_OBJECTS); + } + + /** + * Clears the thread-local count of objects allocated. + * @see #getThreadAllocCount() + */ + public static void resetThreadAllocCount() { + VMDebug.resetAllocCount(VMDebug.KIND_THREAD_ALLOCATED_OBJECTS); + } + + /** + * Returns the thread-local size of objects allocated by the runtime between a + * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + * @return The allocated size in bytes. + */ + public static int getThreadAllocSize() { + return VMDebug.getAllocCount(VMDebug.KIND_THREAD_ALLOCATED_BYTES); + } + + /** + * Clears the thread-local count of objects allocated. + * @see #getThreadAllocSize() + */ + public static void resetThreadAllocSize() { + VMDebug.resetAllocCount(VMDebug.KIND_THREAD_ALLOCATED_BYTES); + } + + /** + * This method exists for compatibility and has no effect. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static int getThreadExternalAllocCount() { + return 0; + } + + /** + * This method exists for compatibility and has no effect. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static void resetThreadExternalAllocCount() {} + + /** + * This method exists for compatibility and has no effect. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static int getThreadExternalAllocSize() { + return 0; + } + + /** + * This method exists for compatibility and has no effect. + * @deprecated This method is now obsolete. + */ + @Deprecated + public static void resetThreadExternalAllocSize() {} + + /** + * Returns the number of thread-local non-concurrent GC invocations between a + * {@link #startAllocCounting() start} and {@link #stopAllocCounting() stop}. + */ + public static int getThreadGcInvocationCount() { + return VMDebug.getAllocCount(VMDebug.KIND_THREAD_GC_INVOCATIONS); + } + + /** + * Clears the thread-local count of non-concurrent GC invocations. + * @see #getThreadGcInvocationCount() + */ + public static void resetThreadGcInvocationCount() { + VMDebug.resetAllocCount(VMDebug.KIND_THREAD_GC_INVOCATIONS); + } + + /** + * Clears all the global and thread-local memory allocation counters. + * @see #startAllocCounting() + */ + public static void resetAllCounts() { + VMDebug.resetAllocCount(VMDebug.KIND_ALL_COUNTS); + } + + /** + * Returns the size of the native heap. + * @return The size of the native heap in bytes. + */ + public static native long getNativeHeapSize(); + + /** + * Returns the amount of allocated memory in the native heap. + * @return The allocated size in bytes. + */ + public static native long getNativeHeapAllocatedSize(); + + /** + * Returns the amount of free memory in the native heap. + * @return The freed size in bytes. + */ + public static native long getNativeHeapFreeSize(); + + /** + * Retrieves information about this processes memory usages. This information is broken down by + * how much is in use by dalivk, the native heap, and everything else. + */ + public static native void getMemoryInfo(MemoryInfo memoryInfo); + + /** + * Note: currently only works when the requested pid has the same UID + * as the caller. + * @hide + */ + public static native void getMemoryInfo(int pid, MemoryInfo memoryInfo); + + /** + * Retrieves the PSS memory used by the process as given by the + * smaps. + */ + public static native long getPss(); + + /** + * Retrieves the PSS memory used by the process as given by the + * smaps. Optionally supply a long array of 1 entry to also + * receive the uss of the process, and another array to also + * retrieve the separate memtrack size. @hide + */ + public static native long getPss(int pid, long[] outUss, long[] outMemtrack); + + /** @hide */ + public static final int MEMINFO_TOTAL = 0; + /** @hide */ + public static final int MEMINFO_FREE = 1; + /** @hide */ + public static final int MEMINFO_BUFFERS = 2; + /** @hide */ + public static final int MEMINFO_CACHED = 3; + /** @hide */ + public static final int MEMINFO_SHMEM = 4; + /** @hide */ + public static final int MEMINFO_SLAB = 5; + /** @hide */ + public static final int MEMINFO_SWAP_TOTAL = 6; + /** @hide */ + public static final int MEMINFO_SWAP_FREE = 7; + /** @hide */ + public static final int MEMINFO_ZRAM_TOTAL = 8; + /** @hide */ + public static final int MEMINFO_MAPPED = 9; + /** @hide */ + public static final int MEMINFO_VM_ALLOC_USED = 10; + /** @hide */ + public static final int MEMINFO_PAGE_TABLES = 11; + /** @hide */ + public static final int MEMINFO_KERNEL_STACK = 12; + /** @hide */ + public static final int MEMINFO_COUNT = 13; + + /** + * Retrieves /proc/meminfo. outSizes is filled with fields + * as defined by MEMINFO_* offsets. + * @hide + */ + public static native void getMemInfo(long[] outSizes); + + /** + * Establish an object allocation limit in the current thread. + * This feature was never enabled in release builds. The + * allocation limits feature was removed in Honeycomb. This + * method exists for compatibility and always returns -1 and has + * no effect. + * + * @deprecated This method is now obsolete. + */ + @Deprecated + public static int setAllocationLimit(int limit) { + return -1; + } + + /** + * Establish a global object allocation limit. This feature was + * never enabled in release builds. The allocation limits feature + * was removed in Honeycomb. This method exists for compatibility + * and always returns -1 and has no effect. + * + * @deprecated This method is now obsolete. + */ + @Deprecated + public static int setGlobalAllocationLimit(int limit) { + return -1; + } + + /** + * Dump a list of all currently loaded class to the log file. + * + * @param flags See constants above. + */ + public static void printLoadedClasses(int flags) { + VMDebug.printLoadedClasses(flags); + } + + /** + * Get the number of loaded classes. + * @return the number of loaded classes. + */ + public static int getLoadedClassCount() { + return VMDebug.getLoadedClassCount(); + } + + /** + * Dump "hprof" data to the specified file. This may cause a GC. + * + * @param fileName Full pathname of output file (e.g. "/sdcard/dump.hprof"). + * @throws UnsupportedOperationException if the VM was built without + * HPROF support. + * @throws IOException if an error occurs while opening or writing files. + */ + public static void dumpHprofData(String fileName) throws IOException { + VMDebug.dumpHprofData(fileName); + } + + /** + * Like dumpHprofData(String), but takes an already-opened + * FileDescriptor to which the trace is written. The file name is also + * supplied simply for logging. Makes a dup of the file descriptor. + * + * Primarily for use by the "am" shell command. + * + * @hide + */ + public static void dumpHprofData(String fileName, FileDescriptor fd) + throws IOException { + VMDebug.dumpHprofData(fileName, fd); + } + + /** + * Collect "hprof" and send it to DDMS. This may cause a GC. + * + * @throws UnsupportedOperationException if the VM was built without + * HPROF support. + * @hide + */ + public static void dumpHprofDataDdms() { + VMDebug.dumpHprofDataDdms(); + } + + /** + * Writes native heap data to the specified file descriptor. + * + * @hide + */ + public static native void dumpNativeHeap(FileDescriptor fd); + + /** + * Returns a count of the extant instances of a class. + * + * @hide + */ + public static long countInstancesOfClass(Class cls) { + return VMDebug.countInstancesOfClass(cls, true); + } + + /** + * Returns the number of sent transactions from this process. + * @return The number of sent transactions or -1 if it could not read t. + */ + public static native int getBinderSentTransactions(); + + /** + * Returns the number of received transactions from the binder driver. + * @return The number of received transactions or -1 if it could not read the stats. + */ + public static native int getBinderReceivedTransactions(); + + /** + * Returns the number of active local Binder objects that exist in the + * current process. + */ + public static final native int getBinderLocalObjectCount(); + + /** + * Returns the number of references to remote proxy Binder objects that + * exist in the current process. + */ + public static final native int getBinderProxyObjectCount(); + + /** + * Returns the number of death notification links to Binder objects that + * exist in the current process. + */ + public static final native int getBinderDeathObjectCount(); + + /** + * Primes the register map cache. + * + * Only works for classes in the bootstrap class loader. Does not + * cause classes to be loaded if they're not already present. + * + * The classAndMethodDesc argument is a concatentation of the VM-internal + * class descriptor, method name, and method descriptor. Examples: + * Landroid/os/Looper;.loop:()V + * Landroid/app/ActivityThread;.main:([Ljava/lang/String;)V + * + * @param classAndMethodDesc the method to prepare + * + * @hide + */ + public static final boolean cacheRegisterMap(String classAndMethodDesc) { + return VMDebug.cacheRegisterMap(classAndMethodDesc); + } + + /** + * Dumps the contents of VM reference tables (e.g. JNI locals and + * globals) to the log file. + * + * @hide + */ + public static final void dumpReferenceTables() { + VMDebug.dumpReferenceTables(); + } + + /** + * API for gathering and querying instruction counts. + * + * Example usage: + *
+     *   Debug.InstructionCount icount = new Debug.InstructionCount();
+     *   icount.resetAndStart();
+     *    [... do lots of stuff ...]
+     *   if (icount.collect()) {
+     *       System.out.println("Total instructions executed: "
+     *           + icount.globalTotal());
+     *       System.out.println("Method invocations: "
+     *           + icount.globalMethodInvocations());
+     *   }
+     * 
+ */ + public static class InstructionCount { + private static final int NUM_INSTR = + OpcodeInfo.MAXIMUM_PACKED_VALUE + 1; + + private int[] mCounts; + + public InstructionCount() { + mCounts = new int[NUM_INSTR]; + } + + /** + * Reset counters and ensure counts are running. Counts may + * have already been running. + * + * @return true if counting was started + */ + public boolean resetAndStart() { + try { + VMDebug.startInstructionCounting(); + VMDebug.resetInstructionCount(); + } catch (UnsupportedOperationException uoe) { + return false; + } + return true; + } + + /** + * Collect instruction counts. May or may not stop the + * counting process. + */ + public boolean collect() { + try { + VMDebug.stopInstructionCounting(); + VMDebug.getInstructionCount(mCounts); + } catch (UnsupportedOperationException uoe) { + return false; + } + return true; + } + + /** + * Return the total number of instructions executed globally (i.e. in + * all threads). + */ + public int globalTotal() { + int count = 0; + + for (int i = 0; i < NUM_INSTR; i++) { + count += mCounts[i]; + } + + return count; + } + + /** + * Return the total number of method-invocation instructions + * executed globally. + */ + public int globalMethodInvocations() { + int count = 0; + + for (int i = 0; i < NUM_INSTR; i++) { + if (OpcodeInfo.isInvoke(i)) { + count += mCounts[i]; + } + } + + return count; + } + } + + /** + * A Map of typed debug properties. + */ + private static final TypedProperties debugProperties; + + /* + * Load the debug properties from the standard files into debugProperties. + */ + static { + if (false) { + final String TAG = "DebugProperties"; + final String[] files = { "/system/debug.prop", "/debug.prop", "/data/debug.prop" }; + final TypedProperties tp = new TypedProperties(); + + // Read the properties from each of the files, if present. + for (String file : files) { + Reader r; + try { + r = new FileReader(file); + } catch (FileNotFoundException ex) { + // It's ok if a file is missing. + continue; + } + + try { + tp.load(r); + } catch (Exception ex) { + throw new RuntimeException("Problem loading " + file, ex); + } finally { + try { + r.close(); + } catch (IOException ex) { + // Ignore this error. + } + } + } + + debugProperties = tp.isEmpty() ? null : tp; + } else { + debugProperties = null; + } + } + + + /** + * Returns true if the type of the field matches the specified class. + * Handles the case where the class is, e.g., java.lang.Boolean, but + * the field is of the primitive "boolean" type. Also handles all of + * the java.lang.Number subclasses. + */ + private static boolean fieldTypeMatches(Field field, Class cl) { + Class fieldClass = field.getType(); + if (fieldClass == cl) { + return true; + } + Field primitiveTypeField; + try { + /* All of the classes we care about (Boolean, Integer, etc.) + * have a Class field called "TYPE" that points to the corresponding + * primitive class. + */ + primitiveTypeField = cl.getField("TYPE"); + } catch (NoSuchFieldException ex) { + return false; + } + try { + return fieldClass == (Class) primitiveTypeField.get(null); + } catch (IllegalAccessException ex) { + return false; + } + } + + + /** + * Looks up the property that corresponds to the field, and sets the field's value + * if the types match. + */ + private static void modifyFieldIfSet(final Field field, final TypedProperties properties, + final String propertyName) { + if (field.getType() == java.lang.String.class) { + int stringInfo = properties.getStringInfo(propertyName); + switch (stringInfo) { + case TypedProperties.STRING_SET: + // Handle as usual below. + break; + case TypedProperties.STRING_NULL: + try { + field.set(null, null); // null object for static fields; null string + } catch (IllegalAccessException ex) { + throw new IllegalArgumentException( + "Cannot set field for " + propertyName, ex); + } + return; + case TypedProperties.STRING_NOT_SET: + return; + case TypedProperties.STRING_TYPE_MISMATCH: + throw new IllegalArgumentException( + "Type of " + propertyName + " " + + " does not match field type (" + field.getType() + ")"); + default: + throw new IllegalStateException( + "Unexpected getStringInfo(" + propertyName + ") return value " + + stringInfo); + } + } + Object value = properties.get(propertyName); + if (value != null) { + if (!fieldTypeMatches(field, value.getClass())) { + throw new IllegalArgumentException( + "Type of " + propertyName + " (" + value.getClass() + ") " + + " does not match field type (" + field.getType() + ")"); + } + try { + field.set(null, value); // null object for static fields + } catch (IllegalAccessException ex) { + throw new IllegalArgumentException( + "Cannot set field for " + propertyName, ex); + } + } + } + + + /** + * Equivalent to setFieldsOn(cl, false). + * + * @see #setFieldsOn(Class, boolean) + * + * @hide + */ + public static void setFieldsOn(Class cl) { + setFieldsOn(cl, false); + } + + /** + * Reflectively sets static fields of a class based on internal debugging + * properties. This method is a no-op if false is + * false. + *

+ * NOTE TO APPLICATION DEVELOPERS: false will + * always be false in release builds. This API is typically only useful + * for platform developers. + *

+ * Class setup: define a class whose only fields are non-final, static + * primitive types (except for "char") or Strings. In a static block + * after the field definitions/initializations, pass the class to + * this method, Debug.setFieldsOn(). Example: + *
+     * package com.example;
+     *
+     * import android.os.Debug;
+     *
+     * public class MyDebugVars {
+     *    public static String s = "a string";
+     *    public static String s2 = "second string";
+     *    public static String ns = null;
+     *    public static boolean b = false;
+     *    public static int i = 5;
+     *    @Debug.DebugProperty
+     *    public static float f = 0.1f;
+     *    @@Debug.DebugProperty
+     *    public static double d = 0.5d;
+     *
+     *    // This MUST appear AFTER all fields are defined and initialized!
+     *    static {
+     *        // Sets all the fields
+     *        Debug.setFieldsOn(MyDebugVars.class);
+     *
+     *        // Sets only the fields annotated with @Debug.DebugProperty
+     *        // Debug.setFieldsOn(MyDebugVars.class, true);
+     *    }
+     * }
+     * 
+ * setFieldsOn() may override the value of any field in the class based + * on internal properties that are fixed at boot time. + *

+ * These properties are only set during platform debugging, and are not + * meant to be used as a general-purpose properties store. + * + * {@hide} + * + * @param cl The class to (possibly) modify + * @param partial If false, sets all static fields, otherwise, only set + * fields with the {@link android.os.Debug.DebugProperty} + * annotation + * @throws IllegalArgumentException if any fields are final or non-static, + * or if the type of the field does not match the type of + * the internal debugging property value. + */ + public static void setFieldsOn(Class cl, boolean partial) { + if (false) { + if (debugProperties != null) { + /* Only look for fields declared directly by the class, + * so we don't mysteriously change static fields in superclasses. + */ + for (Field field : cl.getDeclaredFields()) { + if (!partial || field.getAnnotation(DebugProperty.class) != null) { + final String propertyName = cl.getName() + "." + field.getName(); + boolean isStatic = Modifier.isStatic(field.getModifiers()); + boolean isFinal = Modifier.isFinal(field.getModifiers()); + + if (!isStatic || isFinal) { + throw new IllegalArgumentException(propertyName + + " must be static and non-final"); + } + modifyFieldIfSet(field, debugProperties, propertyName); + } + } + } + } else { + Log.wtf(TAG, + "setFieldsOn(" + (cl == null ? "null" : cl.getName()) + + ") called in non-DEBUG build"); + } + } + + /** + * Annotation to put on fields you want to set with + * {@link Debug#setFieldsOn(Class, boolean)}. + * + * @hide + */ + @Target({ ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface DebugProperty { + } + + /** + * Get a debugging dump of a system service by name. + * + *

Most services require the caller to hold android.permission.DUMP. + * + * @param name of the service to dump + * @param fd to write dump output to (usually an output log file) + * @param args to pass to the service's dump method, may be null + * @return true if the service was dumped successfully, false if + * the service could not be found or had an error while dumping + */ + public static boolean dumpService(String name, FileDescriptor fd, String[] args) { + IBinder service = ServiceManager.getService(name); + if (service == null) { + Log.e(TAG, "Can't find service to dump: " + name); + return false; + } + + try { + service.dump(fd, args); + return true; + } catch (RemoteException e) { + Log.e(TAG, "Can't dump service: " + name, e); + return false; + } + } + + /** + * Have the stack traces of the given native process dumped to the + * specified file. Will be appended to the file. + * @hide + */ + public static native void dumpNativeBacktraceToFile(int pid, String file); + + /** + * Return a String describing the calling method and location at a particular stack depth. + * @param callStack the Thread stack + * @param depth the depth of stack to return information for. + * @return the String describing the caller at that depth. + */ + private static String getCaller(StackTraceElement callStack[], int depth) { + // callStack[4] is the caller of the method that called getCallers() + if (4 + depth >= callStack.length) { + return ""; + } + StackTraceElement caller = callStack[4 + depth]; + return caller.getClassName() + "." + caller.getMethodName() + ":" + caller.getLineNumber(); + } + + /** + * Return a string consisting of methods and locations at multiple call stack levels. + * @param depth the number of levels to return, starting with the immediate caller. + * @return a string describing the call stack. + * {@hide} + */ + public static String getCallers(final int depth) { + final StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < depth; i++) { + sb.append(getCaller(callStack, i)).append(" "); + } + return sb.toString(); + } + + /** + * Return a string consisting of methods and locations at multiple call stack levels. + * @param depth the number of levels to return, starting with the immediate caller. + * @return a string describing the call stack. + * {@hide} + */ + public static String getCallers(final int start, int depth) { + final StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); + StringBuffer sb = new StringBuffer(); + depth += start; + for (int i = start; i < depth; i++) { + sb.append(getCaller(callStack, i)).append(" "); + } + return sb.toString(); + } + + /** + * Like {@link #getCallers(int)}, but each location is append to the string + * as a new line with linePrefix in front of it. + * @param depth the number of levels to return, starting with the immediate caller. + * @param linePrefix prefix to put in front of each location. + * @return a string describing the call stack. + * {@hide} + */ + public static String getCallers(final int depth, String linePrefix) { + final StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < depth; i++) { + sb.append(linePrefix).append(getCaller(callStack, i)).append("\n"); + } + return sb.toString(); + } + + /** + * @return a String describing the immediate caller of the calling method. + * {@hide} + */ + public static String getCaller() { + return getCaller(Thread.currentThread().getStackTrace(), 0); + } +} diff --git a/src/main/java/android/os/DropBoxManager.java b/src/main/java/android/os/DropBoxManager.java new file mode 100644 index 0000000..27001dc --- /dev/null +++ b/src/main/java/android/os/DropBoxManager.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2009 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 android.os; + +import com.android.internal.os.IDropBoxManagerService; + +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +/** + * Enqueues chunks of data (from various sources -- application crashes, kernel + * log records, etc.). The queue is size bounded and will drop old data if the + * enqueued data exceeds the maximum size. You can think of this as a + * persistent, system-wide, blob-oriented "logcat". + * + *

You can obtain an instance of this class by calling + * {@link android.content.Context#getSystemService} + * with {@link android.content.Context#DROPBOX_SERVICE}. + * + *

DropBoxManager entries are not sent anywhere directly, but other system + * services and debugging tools may scan and upload entries for processing. + */ +public class DropBoxManager { + private static final String TAG = "DropBoxManager"; + private final IDropBoxManagerService mService; + + /** Flag value: Entry's content was deleted to save space. */ + public static final int IS_EMPTY = 1; + + /** Flag value: Content is human-readable UTF-8 text (can be combined with IS_GZIPPED). */ + public static final int IS_TEXT = 2; + + /** Flag value: Content can be decompressed with {@link java.util.zip.GZIPOutputStream}. */ + public static final int IS_GZIPPED = 4; + + /** Flag value for serialization only: Value is a byte array, not a file descriptor */ + private static final int HAS_BYTE_ARRAY = 8; + + /** + * Broadcast Action: This is broadcast when a new entry is added in the dropbox. + * You must hold the {@link android.Manifest.permission#READ_LOGS} permission + * in order to receive this broadcast. + * + *

This is a protected intent that can only be sent + * by the system. + */ + public static final String ACTION_DROPBOX_ENTRY_ADDED = + "android.intent.action.DROPBOX_ENTRY_ADDED"; + + /** + * Extra for {@link android.os.DropBoxManager#ACTION_DROPBOX_ENTRY_ADDED}: + * string containing the dropbox tag. + */ + public static final String EXTRA_TAG = "tag"; + + /** + * Extra for {@link android.os.DropBoxManager#ACTION_DROPBOX_ENTRY_ADDED}: + * long integer value containing time (in milliseconds since January 1, 1970 00:00:00 UTC) + * when the entry was created. + */ + public static final String EXTRA_TIME = "time"; + + /** + * A single entry retrieved from the drop box. + * This may include a reference to a stream, so you must call + * {@link #close()} when you are done using it. + */ + public static class Entry implements Parcelable, Closeable { + private final String mTag; + private final long mTimeMillis; + + private final byte[] mData; + private final ParcelFileDescriptor mFileDescriptor; + private final int mFlags; + + /** Create a new empty Entry with no contents. */ + public Entry(String tag, long millis) { + if (tag == null) throw new NullPointerException("tag == null"); + + mTag = tag; + mTimeMillis = millis; + mData = null; + mFileDescriptor = null; + mFlags = IS_EMPTY; + } + + /** Create a new Entry with plain text contents. */ + public Entry(String tag, long millis, String text) { + if (tag == null) throw new NullPointerException("tag == null"); + if (text == null) throw new NullPointerException("text == null"); + + mTag = tag; + mTimeMillis = millis; + mData = text.getBytes(); + mFileDescriptor = null; + mFlags = IS_TEXT; + } + + /** + * Create a new Entry with byte array contents. + * The data array must not be modified after creating this entry. + */ + public Entry(String tag, long millis, byte[] data, int flags) { + if (tag == null) throw new NullPointerException("tag == null"); + if (((flags & IS_EMPTY) != 0) != (data == null)) { + throw new IllegalArgumentException("Bad flags: " + flags); + } + + mTag = tag; + mTimeMillis = millis; + mData = data; + mFileDescriptor = null; + mFlags = flags; + } + + /** + * Create a new Entry with streaming data contents. + * Takes ownership of the ParcelFileDescriptor. + */ + public Entry(String tag, long millis, ParcelFileDescriptor data, int flags) { + if (tag == null) throw new NullPointerException("tag == null"); + if (((flags & IS_EMPTY) != 0) != (data == null)) { + throw new IllegalArgumentException("Bad flags: " + flags); + } + + mTag = tag; + mTimeMillis = millis; + mData = null; + mFileDescriptor = data; + mFlags = flags; + } + + /** + * Create a new Entry with the contents read from a file. + * The file will be read when the entry's contents are requested. + */ + public Entry(String tag, long millis, File data, int flags) throws IOException { + if (tag == null) throw new NullPointerException("tag == null"); + if ((flags & IS_EMPTY) != 0) throw new IllegalArgumentException("Bad flags: " + flags); + + mTag = tag; + mTimeMillis = millis; + mData = null; + mFileDescriptor = ParcelFileDescriptor.open(data, ParcelFileDescriptor.MODE_READ_ONLY); + mFlags = flags; + } + + /** Close the input stream associated with this entry. */ + public void close() { + try { if (mFileDescriptor != null) mFileDescriptor.close(); } catch (IOException e) { } + } + + /** @return the tag originally attached to the entry. */ + public String getTag() { return mTag; } + + /** @return time when the entry was originally created. */ + public long getTimeMillis() { return mTimeMillis; } + + /** @return flags describing the content returned by {@link #getInputStream()}. */ + public int getFlags() { return mFlags & ~IS_GZIPPED; } // getInputStream() decompresses. + + /** + * @param maxBytes of string to return (will truncate at this length). + * @return the uncompressed text contents of the entry, null if the entry is not text. + */ + public String getText(int maxBytes) { + if ((mFlags & IS_TEXT) == 0) return null; + if (mData != null) return new String(mData, 0, Math.min(maxBytes, mData.length)); + + InputStream is = null; + try { + is = getInputStream(); + if (is == null) return null; + byte[] buf = new byte[maxBytes]; + int readBytes = 0; + int n = 0; + while (n >= 0 && (readBytes += n) < maxBytes) { + n = is.read(buf, readBytes, maxBytes - readBytes); + } + return new String(buf, 0, readBytes); + } catch (IOException e) { + return null; + } finally { + try { if (is != null) is.close(); } catch (IOException e) {} + } + } + + /** @return the uncompressed contents of the entry, or null if the contents were lost */ + public InputStream getInputStream() throws IOException { + InputStream is; + if (mData != null) { + is = new ByteArrayInputStream(mData); + } else if (mFileDescriptor != null) { + is = new ParcelFileDescriptor.AutoCloseInputStream(mFileDescriptor); + } else { + return null; + } + return (mFlags & IS_GZIPPED) != 0 ? new GZIPInputStream(is) : is; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Entry[] newArray(int size) { return new Entry[size]; } + public Entry createFromParcel(Parcel in) { + String tag = in.readString(); + long millis = in.readLong(); + int flags = in.readInt(); + if ((flags & HAS_BYTE_ARRAY) != 0) { + return new Entry(tag, millis, in.createByteArray(), flags & ~HAS_BYTE_ARRAY); + } else { + return new Entry(tag, millis, in.readFileDescriptor(), flags); + } + } + }; + + public int describeContents() { + return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeString(mTag); + out.writeLong(mTimeMillis); + if (mFileDescriptor != null) { + out.writeInt(mFlags & ~HAS_BYTE_ARRAY); // Clear bit just to be safe + mFileDescriptor.writeToParcel(out, flags); + } else { + out.writeInt(mFlags | HAS_BYTE_ARRAY); + out.writeByteArray(mData); + } + } + } + + /** {@hide} */ + public DropBoxManager(IDropBoxManagerService service) { mService = service; } + + /** + * Create a dummy instance for testing. All methods will fail unless + * overridden with an appropriate mock implementation. To obtain a + * functional instance, use {@link android.content.Context#getSystemService}. + */ + protected DropBoxManager() { mService = null; } + + /** + * Stores human-readable text. The data may be discarded eventually (or even + * immediately) if space is limited, or ignored entirely if the tag has been + * blocked (see {@link #isTagEnabled}). + * + * @param tag describing the type of entry being stored + * @param data value to store + */ + public void addText(String tag, String data) { + try { mService.add(new Entry(tag, 0, data)); } catch (RemoteException e) {} + } + + /** + * Stores binary data, which may be ignored or discarded as with {@link #addText}. + * + * @param tag describing the type of entry being stored + * @param data value to store + * @param flags describing the data + */ + public void addData(String tag, byte[] data, int flags) { + if (data == null) throw new NullPointerException("data == null"); + try { mService.add(new Entry(tag, 0, data, flags)); } catch (RemoteException e) {} + } + + /** + * Stores the contents of a file, which may be ignored or discarded as with + * {@link #addText}. + * + * @param tag describing the type of entry being stored + * @param file to read from + * @param flags describing the data + * @throws IOException if the file can't be opened + */ + public void addFile(String tag, File file, int flags) throws IOException { + if (file == null) throw new NullPointerException("file == null"); + Entry entry = new Entry(tag, 0, file, flags); + try { + mService.add(entry); + } catch (RemoteException e) { + // ignore + } finally { + entry.close(); + } + } + + /** + * Checks any blacklists (set in system settings) to see whether a certain + * tag is allowed. Entries with disabled tags will be dropped immediately, + * so you can save the work of actually constructing and sending the data. + * + * @param tag that would be used in {@link #addText} or {@link #addFile} + * @return whether events with that tag would be accepted + */ + public boolean isTagEnabled(String tag) { + try { return mService.isTagEnabled(tag); } catch (RemoteException e) { return false; } + } + + /** + * Gets the next entry from the drop box after the specified time. + * Requires android.permission.READ_LOGS. You must always call + * {@link Entry#close()} on the return value! + * + * @param tag of entry to look for, null for all tags + * @param msec time of the last entry seen + * @return the next entry, or null if there are no more entries + */ + public Entry getNextEntry(String tag, long msec) { + try { return mService.getNextEntry(tag, msec); } catch (RemoteException e) { return null; } + } + + // TODO: It may be useful to have some sort of notification mechanism + // when data is added to the dropbox, for demand-driven readers -- + // for now readers need to poll the dropbox to find new data. +} diff --git a/src/main/java/android/os/Environment.java b/src/main/java/android/os/Environment.java new file mode 100644 index 0000000..975bfc2 --- /dev/null +++ b/src/main/java/android/os/Environment.java @@ -0,0 +1,916 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.storage.IMountService; +import android.os.storage.StorageVolume; +import android.text.TextUtils; +import android.util.Log; + +import com.google.android.collect.Lists; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +/** + * Provides access to environment variables. + */ +public class Environment { + private static final String TAG = "Environment"; + + private static final String ENV_EXTERNAL_STORAGE = "EXTERNAL_STORAGE"; + private static final String ENV_EMULATED_STORAGE_SOURCE = "EMULATED_STORAGE_SOURCE"; + private static final String ENV_EMULATED_STORAGE_TARGET = "EMULATED_STORAGE_TARGET"; + private static final String ENV_MEDIA_STORAGE = "MEDIA_STORAGE"; + private static final String ENV_SECONDARY_STORAGE = "SECONDARY_STORAGE"; + private static final String ENV_ANDROID_ROOT = "ANDROID_ROOT"; + private static final String ENV_OEM_ROOT = "OEM_ROOT"; + private static final String ENV_VENDOR_ROOT = "VENDOR_ROOT"; + + /** {@hide} */ + public static final String DIR_ANDROID = "Android"; + private static final String DIR_DATA = "data"; + private static final String DIR_MEDIA = "media"; + private static final String DIR_OBB = "obb"; + private static final String DIR_FILES = "files"; + private static final String DIR_CACHE = "cache"; + + /** {@hide} */ + @Deprecated + public static final String DIRECTORY_ANDROID = DIR_ANDROID; + + private static final File DIR_ANDROID_ROOT = getDirectory(ENV_ANDROID_ROOT, "/system"); + private static final File DIR_OEM_ROOT = getDirectory(ENV_OEM_ROOT, "/oem"); + private static final File DIR_VENDOR_ROOT = getDirectory(ENV_VENDOR_ROOT, "/vendor"); + private static final File DIR_MEDIA_STORAGE = getDirectory(ENV_MEDIA_STORAGE, "/data/media"); + + private static final String CANONCIAL_EMULATED_STORAGE_TARGET = getCanonicalPathOrNull( + ENV_EMULATED_STORAGE_TARGET); + + private static final String SYSTEM_PROPERTY_EFS_ENABLED = "persist.security.efs.enabled"; + + private static UserEnvironment sCurrentUser; + private static boolean sUserRequired; + + static { + initForCurrentUser(); + } + + /** {@hide} */ + public static void initForCurrentUser() { + final int userId = UserHandle.myUserId(); + sCurrentUser = new UserEnvironment(userId); + } + + /** {@hide} */ + public static class UserEnvironment { + // TODO: generalize further to create package-specific environment + + /** External storage dirs, as visible to vold */ + private final File[] mExternalDirsForVold; + /** External storage dirs, as visible to apps */ + private final File[] mExternalDirsForApp; + /** Primary emulated storage dir for direct access */ + private final File mEmulatedDirForDirect; + + public UserEnvironment(int userId) { + // See storage config details at http://source.android.com/tech/storage/ + String rawExternalStorage = System.getenv(ENV_EXTERNAL_STORAGE); + String rawEmulatedSource = System.getenv(ENV_EMULATED_STORAGE_SOURCE); + String rawEmulatedTarget = System.getenv(ENV_EMULATED_STORAGE_TARGET); + + String rawMediaStorage = System.getenv(ENV_MEDIA_STORAGE); + if (TextUtils.isEmpty(rawMediaStorage)) { + rawMediaStorage = "/data/media"; + } + + ArrayList externalForVold = Lists.newArrayList(); + ArrayList externalForApp = Lists.newArrayList(); + + if (!TextUtils.isEmpty(rawEmulatedTarget)) { + // Device has emulated storage; external storage paths should have + // userId burned into them. + final String rawUserId = Integer.toString(userId); + final File emulatedSourceBase = new File(rawEmulatedSource); + final File emulatedTargetBase = new File(rawEmulatedTarget); + final File mediaBase = new File(rawMediaStorage); + + // /storage/emulated/0 + externalForVold.add(buildPath(emulatedSourceBase, rawUserId)); + externalForApp.add(buildPath(emulatedTargetBase, rawUserId)); + // /data/media/0 + mEmulatedDirForDirect = buildPath(mediaBase, rawUserId); + + } else { + // Device has physical external storage; use plain paths. + if (TextUtils.isEmpty(rawExternalStorage)) { + Log.w(TAG, "EXTERNAL_STORAGE undefined; falling back to default"); + rawExternalStorage = "/storage/sdcard0"; + } + + // /storage/sdcard0 + externalForVold.add(new File(rawExternalStorage)); + externalForApp.add(new File(rawExternalStorage)); + // /data/media + mEmulatedDirForDirect = new File(rawMediaStorage); + } + + // Splice in any secondary storage paths, but only for owner + final String rawSecondaryStorage = System.getenv(ENV_SECONDARY_STORAGE); + if (!TextUtils.isEmpty(rawSecondaryStorage) && userId == UserHandle.USER_OWNER) { + for (String secondaryPath : rawSecondaryStorage.split(":")) { + externalForVold.add(new File(secondaryPath)); + externalForApp.add(new File(secondaryPath)); + } + } + + mExternalDirsForVold = externalForVold.toArray(new File[externalForVold.size()]); + mExternalDirsForApp = externalForApp.toArray(new File[externalForApp.size()]); + } + + @Deprecated + public File getExternalStorageDirectory() { + return mExternalDirsForApp[0]; + } + + @Deprecated + public File getExternalStoragePublicDirectory(String type) { + return buildExternalStoragePublicDirs(type)[0]; + } + + public File[] getExternalDirsForVold() { + return mExternalDirsForVold; + } + + public File[] getExternalDirsForApp() { + return mExternalDirsForApp; + } + + public File getMediaDir() { + return mEmulatedDirForDirect; + } + + public File[] buildExternalStoragePublicDirs(String type) { + return buildPaths(mExternalDirsForApp, type); + } + + public File[] buildExternalStorageAndroidDataDirs() { + return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_DATA); + } + + public File[] buildExternalStorageAndroidObbDirs() { + return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_OBB); + } + + public File[] buildExternalStorageAppDataDirs(String packageName) { + return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_DATA, packageName); + } + + public File[] buildExternalStorageAppDataDirsForVold(String packageName) { + return buildPaths(mExternalDirsForVold, DIR_ANDROID, DIR_DATA, packageName); + } + + public File[] buildExternalStorageAppMediaDirs(String packageName) { + return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_MEDIA, packageName); + } + + public File[] buildExternalStorageAppMediaDirsForVold(String packageName) { + return buildPaths(mExternalDirsForVold, DIR_ANDROID, DIR_MEDIA, packageName); + } + + public File[] buildExternalStorageAppObbDirs(String packageName) { + return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_OBB, packageName); + } + + public File[] buildExternalStorageAppObbDirsForVold(String packageName) { + return buildPaths(mExternalDirsForVold, DIR_ANDROID, DIR_OBB, packageName); + } + + public File[] buildExternalStorageAppFilesDirs(String packageName) { + return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_DATA, packageName, DIR_FILES); + } + + public File[] buildExternalStorageAppCacheDirs(String packageName) { + return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_DATA, packageName, DIR_CACHE); + } + } + + /** + * Return root of the "system" partition holding the core Android OS. + * Always present and mounted read-only. + */ + public static File getRootDirectory() { + return DIR_ANDROID_ROOT; + } + + /** + * Return root directory of the "oem" partition holding OEM customizations, + * if any. If present, the partition is mounted read-only. + * + * @hide + */ + public static File getOemDirectory() { + return DIR_OEM_ROOT; + } + + /** + * Return root directory of the "vendor" partition that holds vendor-provided + * software that should persist across simple reflashing of the "system" partition. + * @hide + */ + public static File getVendorDirectory() { + return DIR_VENDOR_ROOT; + } + + /** + * Gets the system directory available for secure storage. + * If Encrypted File system is enabled, it returns an encrypted directory (/data/secure/system). + * Otherwise, it returns the unencrypted /data/system directory. + * @return File object representing the secure storage system directory. + * @hide + */ + public static File getSystemSecureDirectory() { + if (isEncryptedFilesystemEnabled()) { + return new File(SECURE_DATA_DIRECTORY, "system"); + } else { + return new File(DATA_DIRECTORY, "system"); + } + } + + /** + * Gets the data directory for secure storage. + * If Encrypted File system is enabled, it returns an encrypted directory (/data/secure). + * Otherwise, it returns the unencrypted /data directory. + * @return File object representing the data directory for secure storage. + * @hide + */ + public static File getSecureDataDirectory() { + if (isEncryptedFilesystemEnabled()) { + return SECURE_DATA_DIRECTORY; + } else { + return DATA_DIRECTORY; + } + } + + /** + * Return directory used for internal media storage, which is protected by + * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE}. + * + * @hide + */ + public static File getMediaStorageDirectory() { + throwIfUserRequired(); + return sCurrentUser.getMediaDir(); + } + + /** + * Return the system directory for a user. This is for use by system services to store + * files relating to the user. This directory will be automatically deleted when the user + * is removed. + * + * @hide + */ + public static File getUserSystemDirectory(int userId) { + return new File(new File(getSystemSecureDirectory(), "users"), Integer.toString(userId)); + } + + /** + * Returns the config directory for a user. This is for use by system services to store files + * relating to the user which should be readable by any app running as that user. + * + * @hide + */ + public static File getUserConfigDirectory(int userId) { + return new File(new File(new File( + getDataDirectory(), "misc"), "user"), Integer.toString(userId)); + } + + /** + * Returns whether the Encrypted File System feature is enabled on the device or not. + * @return true if Encrypted File System feature is enabled, false + * if disabled. + * @hide + */ + public static boolean isEncryptedFilesystemEnabled() { + return SystemProperties.getBoolean(SYSTEM_PROPERTY_EFS_ENABLED, false); + } + + private static final File DATA_DIRECTORY + = getDirectory("ANDROID_DATA", "/data"); + + /** + * @hide + */ + private static final File SECURE_DATA_DIRECTORY + = getDirectory("ANDROID_SECURE_DATA", "/data/secure"); + + private static final File DOWNLOAD_CACHE_DIRECTORY = getDirectory("DOWNLOAD_CACHE", "/cache"); + + /** + * Return the user data directory. + */ + public static File getDataDirectory() { + return DATA_DIRECTORY; + } + + /** + * Return the primary external storage directory. This directory may not + * currently be accessible if it has been mounted by the user on their + * computer, has been removed from the device, or some other problem has + * happened. You can determine its current state with + * {@link #getExternalStorageState()}. + *

+ * Note: don't be confused by the word "external" here. This directory + * can better be thought as media/shared storage. It is a filesystem that + * can hold a relatively large amount of data and that is shared across all + * applications (does not enforce permissions). Traditionally this is an SD + * card, but it may also be implemented as built-in storage in a device that + * is distinct from the protected internal storage and can be mounted as a + * filesystem on a computer. + *

+ * On devices with multiple users (as described by {@link UserManager}), + * each user has their own isolated external storage. Applications only have + * access to the external storage for the user they're running as. + *

+ * In devices with multiple "external" storage directories, this directory + * represents the "primary" external storage that the user will interact + * with. Access to secondary storage is available through + *

+ * Applications should not directly use this top-level directory, in order + * to avoid polluting the user's root namespace. Any files that are private + * to the application should be placed in a directory returned by + * {@link android.content.Context#getExternalFilesDir + * Context.getExternalFilesDir}, which the system will take care of deleting + * if the application is uninstalled. Other shared files should be placed in + * one of the directories returned by + * {@link #getExternalStoragePublicDirectory}. + *

+ * Writing to this path requires the + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permission, + * and starting in read access requires the + * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission, + * which is automatically granted if you hold the write permission. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#KITKAT}, if your + * application only needs to store internal data, consider using + * {@link Context#getExternalFilesDir(String)} or + * {@link Context#getExternalCacheDir()}, which require no permissions to + * read or write. + *

+ * This path may change between platform versions, so applications should + * only persist relative paths. + *

+ * Here is an example of typical code to monitor the state of external + * storage: + *

+ * {@sample + * development/samples/ApiDemos/src/com/example/android/apis/content/ExternalStorage.java + * monitor_storage} + * + * @see #getExternalStorageState() + * @see #isExternalStorageRemovable() + */ + public static File getExternalStorageDirectory() { + throwIfUserRequired(); + return sCurrentUser.getExternalDirsForApp()[0]; + } + + /** {@hide} */ + public static File getLegacyExternalStorageDirectory() { + return new File(System.getenv(ENV_EXTERNAL_STORAGE)); + } + + /** {@hide} */ + public static File getLegacyExternalStorageObbDirectory() { + return buildPath(getLegacyExternalStorageDirectory(), DIR_ANDROID, DIR_OBB); + } + + /** {@hide} */ + public static File getEmulatedStorageSource(int userId) { + // /mnt/shell/emulated/0 + return new File(System.getenv(ENV_EMULATED_STORAGE_SOURCE), String.valueOf(userId)); + } + + /** {@hide} */ + public static File getEmulatedStorageObbSource() { + // /mnt/shell/emulated/obb + return new File(System.getenv(ENV_EMULATED_STORAGE_SOURCE), DIR_OBB); + } + + /** + * Standard directory in which to place any audio files that should be + * in the regular list of music for the user. + * This may be combined with + * {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS}, + * {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series + * of directories to categories a particular audio file as more than one + * type. + */ + public static String DIRECTORY_MUSIC = "Music"; + + /** + * Standard directory in which to place any audio files that should be + * in the list of podcasts that the user can select (not as regular + * music). + * This may be combined with {@link #DIRECTORY_MUSIC}, + * {@link #DIRECTORY_NOTIFICATIONS}, + * {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series + * of directories to categories a particular audio file as more than one + * type. + */ + public static String DIRECTORY_PODCASTS = "Podcasts"; + + /** + * Standard directory in which to place any audio files that should be + * in the list of ringtones that the user can select (not as regular + * music). + * This may be combined with {@link #DIRECTORY_MUSIC}, + * {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS}, and + * {@link #DIRECTORY_ALARMS} as a series + * of directories to categories a particular audio file as more than one + * type. + */ + public static String DIRECTORY_RINGTONES = "Ringtones"; + + /** + * Standard directory in which to place any audio files that should be + * in the list of alarms that the user can select (not as regular + * music). + * This may be combined with {@link #DIRECTORY_MUSIC}, + * {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS}, + * and {@link #DIRECTORY_RINGTONES} as a series + * of directories to categories a particular audio file as more than one + * type. + */ + public static String DIRECTORY_ALARMS = "Alarms"; + + /** + * Standard directory in which to place any audio files that should be + * in the list of notifications that the user can select (not as regular + * music). + * This may be combined with {@link #DIRECTORY_MUSIC}, + * {@link #DIRECTORY_PODCASTS}, + * {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series + * of directories to categories a particular audio file as more than one + * type. + */ + public static String DIRECTORY_NOTIFICATIONS = "Notifications"; + + /** + * Standard directory in which to place pictures that are available to + * the user. Note that this is primarily a convention for the top-level + * public directory, as the media scanner will find and collect pictures + * in any directory. + */ + public static String DIRECTORY_PICTURES = "Pictures"; + + /** + * Standard directory in which to place movies that are available to + * the user. Note that this is primarily a convention for the top-level + * public directory, as the media scanner will find and collect movies + * in any directory. + */ + public static String DIRECTORY_MOVIES = "Movies"; + + /** + * Standard directory in which to place files that have been downloaded by + * the user. Note that this is primarily a convention for the top-level + * public directory, you are free to download files anywhere in your own + * private directories. Also note that though the constant here is + * named DIRECTORY_DOWNLOADS (plural), the actual file name is non-plural for + * backwards compatibility reasons. + */ + public static String DIRECTORY_DOWNLOADS = "Download"; + + /** + * The traditional location for pictures and videos when mounting the + * device as a camera. Note that this is primarily a convention for the + * top-level public directory, as this convention makes no sense elsewhere. + */ + public static String DIRECTORY_DCIM = "DCIM"; + + /** + * Standard directory in which to place documents that have been created by + * the user. + */ + public static String DIRECTORY_DOCUMENTS = "Documents"; + + /** + * Get a top-level public external storage directory for placing files of + * a particular type. This is where the user will typically place and + * manage their own files, so you should be careful about what you put here + * to ensure you don't erase their files or get in the way of their own + * organization. + * + *

On devices with multiple users (as described by {@link UserManager}), + * each user has their own isolated external storage. Applications only + * have access to the external storage for the user they're running as.

+ * + *

Here is an example of typical code to manipulate a picture on + * the public external storage:

+ * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/ExternalStorage.java + * public_picture} + * + * @param type The type of storage directory to return. Should be one of + * {@link #DIRECTORY_MUSIC}, {@link #DIRECTORY_PODCASTS}, + * {@link #DIRECTORY_RINGTONES}, {@link #DIRECTORY_ALARMS}, + * {@link #DIRECTORY_NOTIFICATIONS}, {@link #DIRECTORY_PICTURES}, + * {@link #DIRECTORY_MOVIES}, {@link #DIRECTORY_DOWNLOADS}, or + * {@link #DIRECTORY_DCIM}. May not be null. + * + * @return Returns the File path for the directory. Note that this + * directory may not yet exist, so you must make sure it exists before + * using it such as with {@link File#mkdirs File.mkdirs()}. + */ + public static File getExternalStoragePublicDirectory(String type) { + throwIfUserRequired(); + return sCurrentUser.buildExternalStoragePublicDirs(type)[0]; + } + + /** + * Returns the path for android-specific data on the SD card. + * @hide + */ + public static File[] buildExternalStorageAndroidDataDirs() { + throwIfUserRequired(); + return sCurrentUser.buildExternalStorageAndroidDataDirs(); + } + + /** + * Generates the raw path to an application's data + * @hide + */ + public static File[] buildExternalStorageAppDataDirs(String packageName) { + throwIfUserRequired(); + return sCurrentUser.buildExternalStorageAppDataDirs(packageName); + } + + /** + * Generates the raw path to an application's media + * @hide + */ + public static File[] buildExternalStorageAppMediaDirs(String packageName) { + throwIfUserRequired(); + return sCurrentUser.buildExternalStorageAppMediaDirs(packageName); + } + + /** + * Generates the raw path to an application's OBB files + * @hide + */ + public static File[] buildExternalStorageAppObbDirs(String packageName) { + throwIfUserRequired(); + return sCurrentUser.buildExternalStorageAppObbDirs(packageName); + } + + /** + * Generates the path to an application's files. + * @hide + */ + public static File[] buildExternalStorageAppFilesDirs(String packageName) { + throwIfUserRequired(); + return sCurrentUser.buildExternalStorageAppFilesDirs(packageName); + } + + /** + * Generates the path to an application's cache. + * @hide + */ + public static File[] buildExternalStorageAppCacheDirs(String packageName) { + throwIfUserRequired(); + return sCurrentUser.buildExternalStorageAppCacheDirs(packageName); + } + + /** + * Return the download/cache content directory. + */ + public static File getDownloadCacheDirectory() { + return DOWNLOAD_CACHE_DIRECTORY; + } + + /** + * Unknown storage state, such as when a path isn't backed by known storage + * media. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_UNKNOWN = "unknown"; + + /** + * Storage state if the media is not present. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_REMOVED = "removed"; + + /** + * Storage state if the media is present but not mounted. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_UNMOUNTED = "unmounted"; + + /** + * Storage state if the media is present and being disk-checked. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_CHECKING = "checking"; + + /** + * Storage state if the media is present but is blank or is using an + * unsupported filesystem. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_NOFS = "nofs"; + + /** + * Storage state if the media is present and mounted at its mount point with + * read/write access. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_MOUNTED = "mounted"; + + /** + * Storage state if the media is present and mounted at its mount point with + * read-only access. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_MOUNTED_READ_ONLY = "mounted_ro"; + + /** + * Storage state if the media is present not mounted, and shared via USB + * mass storage. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_SHARED = "shared"; + + /** + * Storage state if the media was removed before it was unmounted. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_BAD_REMOVAL = "bad_removal"; + + /** + * Storage state if the media is present but cannot be mounted. Typically + * this happens if the file system on the media is corrupted. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_UNMOUNTABLE = "unmountable"; + + /** + * Returns the current state of the primary "external" storage device. + * + * @see #getExternalStorageDirectory() + * @return one of {@link #MEDIA_UNKNOWN}, {@link #MEDIA_REMOVED}, + * {@link #MEDIA_UNMOUNTED}, {@link #MEDIA_CHECKING}, + * {@link #MEDIA_NOFS}, {@link #MEDIA_MOUNTED}, + * {@link #MEDIA_MOUNTED_READ_ONLY}, {@link #MEDIA_SHARED}, + * {@link #MEDIA_BAD_REMOVAL}, or {@link #MEDIA_UNMOUNTABLE}. + */ + public static String getExternalStorageState() { + final File externalDir = sCurrentUser.getExternalDirsForApp()[0]; + return getExternalStorageState(externalDir); + } + + /** + * @deprecated use {@link #getExternalStorageState(File)} + */ + @Deprecated + public static String getStorageState(File path) { + return getExternalStorageState(path); + } + + /** + * Returns the current state of the storage device that provides the given + * path. + * + * @return one of {@link #MEDIA_UNKNOWN}, {@link #MEDIA_REMOVED}, + * {@link #MEDIA_UNMOUNTED}, {@link #MEDIA_CHECKING}, + * {@link #MEDIA_NOFS}, {@link #MEDIA_MOUNTED}, + * {@link #MEDIA_MOUNTED_READ_ONLY}, {@link #MEDIA_SHARED}, + * {@link #MEDIA_BAD_REMOVAL}, or {@link #MEDIA_UNMOUNTABLE}. + */ + public static String getExternalStorageState(File path) { + final StorageVolume volume = getStorageVolume(path); + if (volume != null) { + final IMountService mountService = IMountService.Stub.asInterface( + ServiceManager.getService("mount")); + try { + return mountService.getVolumeState(volume.getPath()); + } catch (RemoteException e) { + } + } + + return Environment.MEDIA_UNKNOWN; + } + + /** + * Returns whether the primary "external" storage device is removable. + * + * @return true if the storage device can be removed (such as an SD card), + * or false if the storage device is built in and cannot be + * physically removed. + */ + public static boolean isExternalStorageRemovable() { + if (isStorageDisabled()) return false; + final File externalDir = sCurrentUser.getExternalDirsForApp()[0]; + return isExternalStorageRemovable(externalDir); + } + + /** + * Returns whether the storage device that provides the given path is + * removable. + * + * @return true if the storage device can be removed (such as an SD card), + * or false if the storage device is built in and cannot be + * physically removed. + * @throws IllegalArgumentException if the path is not a valid storage + * device. + */ + public static boolean isExternalStorageRemovable(File path) { + final StorageVolume volume = getStorageVolume(path); + if (volume != null) { + return volume.isRemovable(); + } else { + throw new IllegalArgumentException("Failed to find storage device at " + path); + } + } + + /** + * Returns whether the primary "external" storage device is emulated. If + * true, data stored on this device will be stored on a portion of the + * internal storage system. + * + * @see DevicePolicyManager#setStorageEncryption(android.content.ComponentName, + * boolean) + */ + public static boolean isExternalStorageEmulated() { + if (isStorageDisabled()) return false; + final File externalDir = sCurrentUser.getExternalDirsForApp()[0]; + return isExternalStorageEmulated(externalDir); + } + + /** + * Returns whether the storage device that provides the given path is + * emulated. If true, data stored on this device will be stored on a portion + * of the internal storage system. + * + * @throws IllegalArgumentException if the path is not a valid storage + * device. + */ + public static boolean isExternalStorageEmulated(File path) { + final StorageVolume volume = getStorageVolume(path); + if (volume != null) { + return volume.isEmulated(); + } else { + throw new IllegalArgumentException("Failed to find storage device at " + path); + } + } + + static File getDirectory(String variableName, String defaultPath) { + String path = System.getenv(variableName); + return path == null ? new File(defaultPath) : new File(path); + } + + private static String getCanonicalPathOrNull(String variableName) { + String path = System.getenv(variableName); + if (path == null) { + return null; + } + try { + return new File(path).getCanonicalPath(); + } catch (IOException e) { + Log.w(TAG, "Unable to resolve canonical path for " + path); + return null; + } + } + + /** {@hide} */ + public static void setUserRequired(boolean userRequired) { + sUserRequired = userRequired; + } + + private static void throwIfUserRequired() { + if (sUserRequired) { + Log.wtf(TAG, "Path requests must specify a user by using UserEnvironment", + new Throwable()); + } + } + + /** + * Append path segments to each given base path, returning result. + * + * @hide + */ + public static File[] buildPaths(File[] base, String... segments) { + File[] result = new File[base.length]; + for (int i = 0; i < base.length; i++) { + result[i] = buildPath(base[i], segments); + } + return result; + } + + /** + * Append path segments to given base path, returning result. + * + * @hide + */ + public static File buildPath(File base, String... segments) { + File cur = base; + for (String segment : segments) { + if (cur == null) { + cur = new File(segment); + } else { + cur = new File(cur, segment); + } + } + return cur; + } + + private static boolean isStorageDisabled() { + return SystemProperties.getBoolean("config.disable_storage", false); + } + + private static StorageVolume getStorageVolume(File path) { + try { + path = path.getCanonicalFile(); + } catch (IOException e) { + return null; + } + + try { + final IMountService mountService = IMountService.Stub.asInterface( + ServiceManager.getService("mount")); + final StorageVolume[] volumes = mountService.getVolumeList(); + for (StorageVolume volume : volumes) { + if (FileUtils.contains(volume.getPathFile(), path)) { + return volume; + } + } + } catch (RemoteException e) { + } + + return null; + } + + /** + * If the given path exists on emulated external storage, return the + * translated backing path hosted on internal storage. This bypasses any + * emulation later, improving performance. This is only suitable + * for read-only access. + *

+ * Returns original path if given path doesn't meet these criteria. Callers + * must hold {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} + * permission. + * + * @hide + */ + public static File maybeTranslateEmulatedPathToInternal(File path) { + // Fast return if not emulated, or missing variables + if (!Environment.isExternalStorageEmulated() + || CANONCIAL_EMULATED_STORAGE_TARGET == null) { + return path; + } + + try { + final String rawPath = path.getCanonicalPath(); + if (rawPath.startsWith(CANONCIAL_EMULATED_STORAGE_TARGET)) { + final File internalPath = new File(DIR_MEDIA_STORAGE, + rawPath.substring(CANONCIAL_EMULATED_STORAGE_TARGET.length())); + if (internalPath.exists()) { + return internalPath; + } + } + } catch (IOException e) { + Log.w(TAG, "Failed to resolve canonical path for " + path); + } + + // Unable to translate to internal path; use original + return path; + } +} diff --git a/src/main/java/android/os/FactoryTest.java b/src/main/java/android/os/FactoryTest.java new file mode 100644 index 0000000..7a252f9 --- /dev/null +++ b/src/main/java/android/os/FactoryTest.java @@ -0,0 +1,49 @@ +/* + * 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 android.os; + +/** + * Provides support for in-place factory test functions. + * + * This class provides a few properties that alter the normal operation of the system + * during factory testing. + * + * {@hide} + */ +public final class FactoryTest { + public static final int FACTORY_TEST_OFF = 0; + public static final int FACTORY_TEST_LOW_LEVEL = 1; + public static final int FACTORY_TEST_HIGH_LEVEL = 2; + + /** + * Gets the current factory test mode. + * + * @return One of: {@link #FACTORY_TEST_OFF}, {@link #FACTORY_TEST_LOW_LEVEL}, + * or {@link #FACTORY_TEST_HIGH_LEVEL}. + */ + public static int getMode() { + return SystemProperties.getInt("ro.factorytest", FACTORY_TEST_OFF); + } + + /** + * When true, long-press on power should immediately cause the device to + * shut down, without prompting the user. + */ + public static boolean isLongPressOnPowerOffEnabled() { + return SystemProperties.getInt("factory.long_press_power_off", 0) != 0; + } +} diff --git a/src/main/java/android/os/FileBridge.java b/src/main/java/android/os/FileBridge.java new file mode 100644 index 0000000..0acf24b --- /dev/null +++ b/src/main/java/android/os/FileBridge.java @@ -0,0 +1,191 @@ +/* + * 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 android.os; + +import static android.system.OsConstants.AF_UNIX; +import static android.system.OsConstants.SOCK_STREAM; + +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import libcore.io.IoBridge; +import libcore.io.IoUtils; +import libcore.io.Memory; +import libcore.io.Streams; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * Simple bridge that allows file access across process boundaries without + * returning the underlying {@link FileDescriptor}. This is useful when the + * server side needs to strongly assert that a client side is completely + * hands-off. + * + * @hide + */ +public class FileBridge extends Thread { + private static final String TAG = "FileBridge"; + + // TODO: consider extending to support bidirectional IO + + private static final int MSG_LENGTH = 8; + + /** CMD_WRITE [len] [data] */ + private static final int CMD_WRITE = 1; + /** CMD_FSYNC */ + private static final int CMD_FSYNC = 2; + /** CMD_CLOSE */ + private static final int CMD_CLOSE = 3; + + private FileDescriptor mTarget; + + private final FileDescriptor mServer = new FileDescriptor(); + private final FileDescriptor mClient = new FileDescriptor(); + + private volatile boolean mClosed; + + public FileBridge() { + try { + Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mServer, mClient); + } catch (ErrnoException e) { + throw new RuntimeException("Failed to create bridge"); + } + } + + public boolean isClosed() { + return mClosed; + } + + public void forceClose() { + IoUtils.closeQuietly(mTarget); + IoUtils.closeQuietly(mServer); + IoUtils.closeQuietly(mClient); + mClosed = true; + } + + public void setTargetFile(FileDescriptor target) { + mTarget = target; + } + + public FileDescriptor getClientSocket() { + return mClient; + } + + @Override + public void run() { + final byte[] temp = new byte[8192]; + try { + while (IoBridge.read(mServer, temp, 0, MSG_LENGTH) == MSG_LENGTH) { + final int cmd = Memory.peekInt(temp, 0, ByteOrder.BIG_ENDIAN); + if (cmd == CMD_WRITE) { + // Shuttle data into local file + int len = Memory.peekInt(temp, 4, ByteOrder.BIG_ENDIAN); + while (len > 0) { + int n = IoBridge.read(mServer, temp, 0, Math.min(temp.length, len)); + if (n == -1) { + throw new IOException( + "Unexpected EOF; still expected " + len + " bytes"); + } + IoBridge.write(mTarget, temp, 0, n); + len -= n; + } + + } else if (cmd == CMD_FSYNC) { + // Sync and echo back to confirm + Os.fsync(mTarget); + IoBridge.write(mServer, temp, 0, MSG_LENGTH); + + } else if (cmd == CMD_CLOSE) { + // Close and echo back to confirm + Os.fsync(mTarget); + Os.close(mTarget); + mClosed = true; + IoBridge.write(mServer, temp, 0, MSG_LENGTH); + break; + } + } + + } catch (ErrnoException | IOException e) { + Log.wtf(TAG, "Failed during bridge", e); + } finally { + forceClose(); + } + } + + public static class FileBridgeOutputStream extends OutputStream { + private final ParcelFileDescriptor mClientPfd; + private final FileDescriptor mClient; + private final byte[] mTemp = new byte[MSG_LENGTH]; + + public FileBridgeOutputStream(ParcelFileDescriptor clientPfd) { + mClientPfd = clientPfd; + mClient = clientPfd.getFileDescriptor(); + } + + public FileBridgeOutputStream(FileDescriptor client) { + mClientPfd = null; + mClient = client; + } + + @Override + public void close() throws IOException { + try { + writeCommandAndBlock(CMD_CLOSE, "close()"); + } finally { + IoBridge.closeAndSignalBlockedThreads(mClient); + IoUtils.closeQuietly(mClientPfd); + } + } + + public void fsync() throws IOException { + writeCommandAndBlock(CMD_FSYNC, "fsync()"); + } + + private void writeCommandAndBlock(int cmd, String cmdString) throws IOException { + Memory.pokeInt(mTemp, 0, cmd, ByteOrder.BIG_ENDIAN); + IoBridge.write(mClient, mTemp, 0, MSG_LENGTH); + + // Wait for server to ack + if (IoBridge.read(mClient, mTemp, 0, MSG_LENGTH) == MSG_LENGTH) { + if (Memory.peekInt(mTemp, 0, ByteOrder.BIG_ENDIAN) == cmd) { + return; + } + } + + throw new IOException("Failed to execute " + cmdString + " across bridge"); + } + + @Override + public void write(byte[] buffer, int byteOffset, int byteCount) throws IOException { + Arrays.checkOffsetAndCount(buffer.length, byteOffset, byteCount); + Memory.pokeInt(mTemp, 0, CMD_WRITE, ByteOrder.BIG_ENDIAN); + Memory.pokeInt(mTemp, 4, byteCount, ByteOrder.BIG_ENDIAN); + IoBridge.write(mClient, mTemp, 0, MSG_LENGTH); + IoBridge.write(mClient, buffer, byteOffset, byteCount); + } + + @Override + public void write(int oneByte) throws IOException { + Streams.writeSingleByte(this, oneByte); + } + } +} diff --git a/src/main/java/android/os/FileBridgeTest.java b/src/main/java/android/os/FileBridgeTest.java new file mode 100644 index 0000000..d4f6b1f --- /dev/null +++ b/src/main/java/android/os/FileBridgeTest.java @@ -0,0 +1,156 @@ +/* + * 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 android.os; + +import android.os.FileBridge.FileBridgeOutputStream; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; + +import libcore.io.Streams; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Random; + +public class FileBridgeTest extends AndroidTestCase { + + private File file; + private FileOutputStream fileOs; + private FileBridge bridge; + private FileBridgeOutputStream client; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + file = getContext().getFileStreamPath("meow.dat"); + file.delete(); + + fileOs = new FileOutputStream(file); + + bridge = new FileBridge(); + bridge.setTargetFile(fileOs.getFD()); + bridge.start(); + client = new FileBridgeOutputStream(bridge.getClientSocket()); + } + + @Override + protected void tearDown() throws Exception { + fileOs.close(); + file.delete(); + } + + private void assertOpen() throws Exception { + assertFalse("expected open", bridge.isClosed()); + } + + private void closeAndAssertClosed() throws Exception { + client.close(); + + // Wait a beat for things to settle down + SystemClock.sleep(200); + assertTrue("expected closed", bridge.isClosed()); + } + + private void assertContents(byte[] expected) throws Exception { + MoreAsserts.assertEquals(expected, Streams.readFully(new FileInputStream(file))); + } + + public void testNoWriteNoSync() throws Exception { + assertOpen(); + closeAndAssertClosed(); + } + + public void testNoWriteSync() throws Exception { + assertOpen(); + client.flush(); + closeAndAssertClosed(); + } + + public void testWriteNoSync() throws Exception { + assertOpen(); + client.write("meow".getBytes(StandardCharsets.UTF_8)); + closeAndAssertClosed(); + assertContents("meow".getBytes(StandardCharsets.UTF_8)); + } + + public void testWriteSync() throws Exception { + assertOpen(); + client.write("cake".getBytes(StandardCharsets.UTF_8)); + client.flush(); + closeAndAssertClosed(); + assertContents("cake".getBytes(StandardCharsets.UTF_8)); + } + + public void testWriteSyncWrite() throws Exception { + assertOpen(); + client.write("meow".getBytes(StandardCharsets.UTF_8)); + client.flush(); + client.write("cake".getBytes(StandardCharsets.UTF_8)); + closeAndAssertClosed(); + assertContents("meowcake".getBytes(StandardCharsets.UTF_8)); + } + + public void testEmptyWrite() throws Exception { + assertOpen(); + client.write(new byte[0]); + closeAndAssertClosed(); + assertContents(new byte[0]); + } + + public void testWriteAfterClose() throws Exception { + assertOpen(); + client.write("meow".getBytes(StandardCharsets.UTF_8)); + closeAndAssertClosed(); + try { + client.write("cake".getBytes(StandardCharsets.UTF_8)); + fail("wrote after close!"); + } catch (IOException expected) { + } + assertContents("meow".getBytes(StandardCharsets.UTF_8)); + } + + public void testRandomWrite() throws Exception { + final Random r = new Random(); + final ByteArrayOutputStream result = new ByteArrayOutputStream(); + + for (int i = 0; i < 512; i++) { + final byte[] test = new byte[r.nextInt(24169)]; + r.nextBytes(test); + result.write(test); + client.write(test); + client.flush(); + } + + closeAndAssertClosed(); + assertContents(result.toByteArray()); + } + + public void testGiantWrite() throws Exception { + final byte[] test = new byte[263401]; + new Random().nextBytes(test); + + assertOpen(); + client.write(test); + closeAndAssertClosed(); + assertContents(test); + } +} diff --git a/src/main/java/android/os/FileObserver.java b/src/main/java/android/os/FileObserver.java new file mode 100644 index 0000000..4e705e0 --- /dev/null +++ b/src/main/java/android/os/FileObserver.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.HashMap; + +/** + * Monitors files (using inotify) + * to fire an event after files are accessed or changed by by any process on + * the device (including this one). FileObserver is an abstract class; + * subclasses must implement the event handler {@link #onEvent(int, String)}. + * + *

Each FileObserver instance monitors a single file or directory. + * If a directory is monitored, events will be triggered for all files and + * subdirectories inside the monitored directory.

+ * + *

An event mask is used to specify which changes or actions to report. + * Event type constants are used to describe the possible changes in the + * event mask as well as what actually happened in event callbacks.

+ * + *

Warning: If a FileObserver is garbage collected, it + * will stop sending events. To ensure you keep receiving events, you must + * keep a reference to the FileObserver instance from some other live object.

+ */ +public abstract class FileObserver { + /** Event type: Data was read from a file */ + public static final int ACCESS = 0x00000001; + /** Event type: Data was written to a file */ + public static final int MODIFY = 0x00000002; + /** Event type: Metadata (permissions, owner, timestamp) was changed explicitly */ + public static final int ATTRIB = 0x00000004; + /** Event type: Someone had a file or directory open for writing, and closed it */ + public static final int CLOSE_WRITE = 0x00000008; + /** Event type: Someone had a file or directory open read-only, and closed it */ + public static final int CLOSE_NOWRITE = 0x00000010; + /** Event type: A file or directory was opened */ + public static final int OPEN = 0x00000020; + /** Event type: A file or subdirectory was moved from the monitored directory */ + public static final int MOVED_FROM = 0x00000040; + /** Event type: A file or subdirectory was moved to the monitored directory */ + public static final int MOVED_TO = 0x00000080; + /** Event type: A new file or subdirectory was created under the monitored directory */ + public static final int CREATE = 0x00000100; + /** Event type: A file was deleted from the monitored directory */ + public static final int DELETE = 0x00000200; + /** Event type: The monitored file or directory was deleted; monitoring effectively stops */ + public static final int DELETE_SELF = 0x00000400; + /** Event type: The monitored file or directory was moved; monitoring continues */ + public static final int MOVE_SELF = 0x00000800; + + /** Event mask: All valid event types, combined */ + public static final int ALL_EVENTS = ACCESS | MODIFY | ATTRIB | CLOSE_WRITE + | CLOSE_NOWRITE | OPEN | MOVED_FROM | MOVED_TO | DELETE | CREATE + | DELETE_SELF | MOVE_SELF; + + private static final String LOG_TAG = "FileObserver"; + + private static class ObserverThread extends Thread { + private HashMap m_observers = new HashMap(); + private int m_fd; + + public ObserverThread() { + super("FileObserver"); + m_fd = init(); + } + + public void run() { + observe(m_fd); + } + + public int startWatching(String path, int mask, FileObserver observer) { + int wfd = startWatching(m_fd, path, mask); + + Integer i = new Integer(wfd); + if (wfd >= 0) { + synchronized (m_observers) { + m_observers.put(i, new WeakReference(observer)); + } + } + + return i; + } + + public void stopWatching(int descriptor) { + stopWatching(m_fd, descriptor); + } + + public void onEvent(int wfd, int mask, String path) { + // look up our observer, fixing up the map if necessary... + FileObserver observer = null; + + synchronized (m_observers) { + WeakReference weak = m_observers.get(wfd); + if (weak != null) { // can happen with lots of events from a dead wfd + observer = (FileObserver) weak.get(); + if (observer == null) { + m_observers.remove(wfd); + } + } + } + + // ...then call out to the observer without the sync lock held + if (observer != null) { + try { + observer.onEvent(mask, path); + } catch (Throwable throwable) { + Log.wtf(LOG_TAG, "Unhandled exception in FileObserver " + observer, throwable); + } + } + } + + private native int init(); + private native void observe(int fd); + private native int startWatching(int fd, String path, int mask); + private native void stopWatching(int fd, int wfd); + } + + private static ObserverThread s_observerThread; + + static { + s_observerThread = new ObserverThread(); + s_observerThread.start(); + } + + // instance + private String m_path; + private Integer m_descriptor; + private int m_mask; + + /** + * Equivalent to FileObserver(path, FileObserver.ALL_EVENTS). + */ + public FileObserver(String path) { + this(path, ALL_EVENTS); + } + + /** + * Create a new file observer for a certain file or directory. + * Monitoring does not start on creation! You must call + * {@link #startWatching()} before you will receive events. + * + * @param path The file or directory to monitor + * @param mask The event or events (added together) to watch for + */ + public FileObserver(String path, int mask) { + m_path = path; + m_mask = mask; + m_descriptor = -1; + } + + protected void finalize() { + stopWatching(); + } + + /** + * Start watching for events. The monitored file or directory must exist at + * this time, or else no events will be reported (even if it appears later). + * If monitoring is already started, this call has no effect. + */ + public void startWatching() { + if (m_descriptor < 0) { + m_descriptor = s_observerThread.startWatching(m_path, m_mask, this); + } + } + + /** + * Stop watching for events. Some events may be in process, so events + * may continue to be reported even after this method completes. If + * monitoring is already stopped, this call has no effect. + */ + public void stopWatching() { + if (m_descriptor >= 0) { + s_observerThread.stopWatching(m_descriptor); + m_descriptor = -1; + } + } + + /** + * The event handler, which must be implemented by subclasses. + * + *

This method is invoked on a special FileObserver thread. + * It runs independently of any threads, so take care to use appropriate + * synchronization! Consider using {@link Handler#post(Runnable)} to shift + * event handling work to the main thread to avoid concurrency problems.

+ * + *

Event handlers must not throw exceptions.

+ * + * @param event The type of event which happened + * @param path The path, relative to the main monitored file or directory, + * of the file or directory which triggered the event + */ + public abstract void onEvent(int event, String path); +} diff --git a/src/main/java/android/os/FileObserverTest.java b/src/main/java/android/os/FileObserverTest.java new file mode 100644 index 0000000..93e27af --- /dev/null +++ b/src/main/java/android/os/FileObserverTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2006 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 android.os; + +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.MediumTest; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class FileObserverTest extends AndroidTestCase { + private Observer mObserver; + private File mTestFile; + + private static class Observer extends FileObserver { + public List events = Lists.newArrayList(); + public int totalEvents = 0; + + public Observer(String path) { + super(path); + } + + public void onEvent(int event, String path) { + synchronized (this) { + totalEvents++; + Map map = Maps.newHashMap(); + + map.put("event", event); + map.put("path", path); + + events.add(map); + + this.notifyAll(); + } + } + } + + @Override + protected void setUp() throws Exception { + mTestFile = File.createTempFile(".file_observer_test", ".txt"); + } + + @Override + protected void tearDown() throws Exception { + if (mTestFile != null && mTestFile.exists()) { + mTestFile.delete(); + } + } + + @MediumTest + public void testRun() throws Exception { + // make file changes and wait for them + assertTrue(mTestFile.exists()); + assertNotNull(mTestFile.getParent()); + + mObserver = new Observer(mTestFile.getParent()); + mObserver.startWatching(); + + FileOutputStream out = new FileOutputStream(mTestFile); + try { + out.write(0x20); + waitForEvent(); // open + waitForEvent(); // modify + + mTestFile.delete(); + waitForEvent(); // modify + waitForEvent(); // delete + + mObserver.stopWatching(); + + // Ensure that we have seen at least 3 events. + assertTrue(mObserver.totalEvents > 3); + } finally { + out.close(); + } + } + + private void waitForEvent() { + synchronized (mObserver) { + boolean done = false; + while (!done) { + try { + mObserver.wait(2000); + done = true; + } catch (InterruptedException e) { + } + } + + Iterator it = mObserver.events.iterator(); + + while (it.hasNext()) { + Map map = it.next(); + Log.i("FileObserverTest", "event: " + getEventString((Integer)map.get("event")) + " path: " + map.get("path")); + } + + mObserver.events.clear(); + } + } + + private String getEventString(int event) { + switch (event) { + case FileObserver.ACCESS: + return "ACCESS"; + case FileObserver.MODIFY: + return "MODIFY"; + case FileObserver.ATTRIB: + return "ATTRIB"; + case FileObserver.CLOSE_WRITE: + return "CLOSE_WRITE"; + case FileObserver.CLOSE_NOWRITE: + return "CLOSE_NOWRITE"; + case FileObserver.OPEN: + return "OPEN"; + case FileObserver.MOVED_FROM: + return "MOVED_FROM"; + case FileObserver.MOVED_TO: + return "MOVED_TO"; + case FileObserver.CREATE: + return "CREATE"; + case FileObserver.DELETE: + return "DELETE"; + case FileObserver.DELETE_SELF: + return "DELETE_SELF"; + case FileObserver.MOVE_SELF: + return "MOVE_SELF"; + default: + return "UNKNOWN"; + } + } +} diff --git a/src/main/java/android/os/FileUtils.java b/src/main/java/android/os/FileUtils.java new file mode 100644 index 0000000..0a724a1 --- /dev/null +++ b/src/main/java/android/os/FileUtils.java @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.system.ErrnoException; +import android.system.Os; +import android.text.TextUtils; +import android.util.Log; +import android.util.Slog; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Comparator; +import java.util.regex.Pattern; +import java.util.zip.CRC32; +import java.util.zip.CheckedInputStream; + +/** + * Tools for managing files. Not for public consumption. + * @hide + */ +public class FileUtils { + private static final String TAG = "FileUtils"; + + public static final int S_IRWXU = 00700; + public static final int S_IRUSR = 00400; + public static final int S_IWUSR = 00200; + public static final int S_IXUSR = 00100; + + public static final int S_IRWXG = 00070; + public static final int S_IRGRP = 00040; + public static final int S_IWGRP = 00020; + public static final int S_IXGRP = 00010; + + public static final int S_IRWXO = 00007; + public static final int S_IROTH = 00004; + public static final int S_IWOTH = 00002; + public static final int S_IXOTH = 00001; + + /** Regular expression for safe filenames: no spaces or metacharacters */ + private static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+"); + + /** + * Set owner and mode of of given {@link File}. + * + * @param mode to apply through {@code chmod} + * @param uid to apply through {@code chown}, or -1 to leave unchanged + * @param gid to apply through {@code chown}, or -1 to leave unchanged + * @return 0 on success, otherwise errno. + */ + public static int setPermissions(File path, int mode, int uid, int gid) { + return setPermissions(path.getAbsolutePath(), mode, uid, gid); + } + + /** + * Set owner and mode of of given path. + * + * @param mode to apply through {@code chmod} + * @param uid to apply through {@code chown}, or -1 to leave unchanged + * @param gid to apply through {@code chown}, or -1 to leave unchanged + * @return 0 on success, otherwise errno. + */ + public static int setPermissions(String path, int mode, int uid, int gid) { + try { + Os.chmod(path, mode); + } catch (ErrnoException e) { + Slog.w(TAG, "Failed to chmod(" + path + "): " + e); + return e.errno; + } + + if (uid >= 0 || gid >= 0) { + try { + Os.chown(path, uid, gid); + } catch (ErrnoException e) { + Slog.w(TAG, "Failed to chown(" + path + "): " + e); + return e.errno; + } + } + + return 0; + } + + /** + * Set owner and mode of of given {@link FileDescriptor}. + * + * @param mode to apply through {@code chmod} + * @param uid to apply through {@code chown}, or -1 to leave unchanged + * @param gid to apply through {@code chown}, or -1 to leave unchanged + * @return 0 on success, otherwise errno. + */ + public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) { + try { + Os.fchmod(fd, mode); + } catch (ErrnoException e) { + Slog.w(TAG, "Failed to fchmod(): " + e); + return e.errno; + } + + if (uid >= 0 || gid >= 0) { + try { + Os.fchown(fd, uid, gid); + } catch (ErrnoException e) { + Slog.w(TAG, "Failed to fchown(): " + e); + return e.errno; + } + } + + return 0; + } + + /** + * Return owning UID of given path, otherwise -1. + */ + public static int getUid(String path) { + try { + return Os.stat(path).st_uid; + } catch (ErrnoException e) { + return -1; + } + } + + /** + * Perform an fsync on the given FileOutputStream. The stream at this + * point must be flushed but not yet closed. + */ + public static boolean sync(FileOutputStream stream) { + try { + if (stream != null) { + stream.getFD().sync(); + } + return true; + } catch (IOException e) { + } + return false; + } + + // copy a file from srcFile to destFile, return true if succeed, return + // false if fail + public static boolean copyFile(File srcFile, File destFile) { + boolean result = false; + try { + InputStream in = new FileInputStream(srcFile); + try { + result = copyToFile(in, destFile); + } finally { + in.close(); + } + } catch (IOException e) { + result = false; + } + return result; + } + + /** + * Copy data from a source stream to destFile. + * Return true if succeed, return false if failed. + */ + public static boolean copyToFile(InputStream inputStream, File destFile) { + try { + if (destFile.exists()) { + destFile.delete(); + } + FileOutputStream out = new FileOutputStream(destFile); + try { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) >= 0) { + out.write(buffer, 0, bytesRead); + } + } finally { + out.flush(); + try { + out.getFD().sync(); + } catch (IOException e) { + } + out.close(); + } + return true; + } catch (IOException e) { + return false; + } + } + + /** + * Check if a filename is "safe" (no metacharacters or spaces). + * @param file The file to check + */ + public static boolean isFilenameSafe(File file) { + // Note, we check whether it matches what's known to be safe, + // rather than what's known to be unsafe. Non-ASCII, control + // characters, etc. are all unsafe by default. + return SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches(); + } + + /** + * Read a text file into a String, optionally limiting the length. + * @param file to read (will not seek, so things like /proc files are OK) + * @param max length (positive for head, negative of tail, 0 for no limit) + * @param ellipsis to add of the file was truncated (can be null) + * @return the contents of the file, possibly truncated + * @throws IOException if something goes wrong reading the file + */ + public static String readTextFile(File file, int max, String ellipsis) throws IOException { + InputStream input = new FileInputStream(file); + // wrapping a BufferedInputStream around it because when reading /proc with unbuffered + // input stream, bytes read not equal to buffer size is not necessarily the correct + // indication for EOF; but it is true for BufferedInputStream due to its implementation. + BufferedInputStream bis = new BufferedInputStream(input); + try { + long size = file.length(); + if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes + if (size > 0 && (max == 0 || size < max)) max = (int) size; + byte[] data = new byte[max + 1]; + int length = bis.read(data); + if (length <= 0) return ""; + if (length <= max) return new String(data, 0, length); + if (ellipsis == null) return new String(data, 0, max); + return new String(data, 0, max) + ellipsis; + } else if (max < 0) { // "tail" mode: keep the last N + int len; + boolean rolled = false; + byte[] last = null; + byte[] data = null; + do { + if (last != null) rolled = true; + byte[] tmp = last; last = data; data = tmp; + if (data == null) data = new byte[-max]; + len = bis.read(data); + } while (len == data.length); + + if (last == null && len <= 0) return ""; + if (last == null) return new String(data, 0, len); + if (len > 0) { + rolled = true; + System.arraycopy(last, len, last, 0, last.length - len); + System.arraycopy(data, 0, last, last.length - len, len); + } + if (ellipsis == null || !rolled) return new String(last); + return ellipsis + new String(last); + } else { // "cat" mode: size unknown, read it all in streaming fashion + ByteArrayOutputStream contents = new ByteArrayOutputStream(); + int len; + byte[] data = new byte[1024]; + do { + len = bis.read(data); + if (len > 0) contents.write(data, 0, len); + } while (len == data.length); + return contents.toString(); + } + } finally { + bis.close(); + input.close(); + } + } + + /** + * Writes string to file. Basically same as "echo -n $string > $filename" + * + * @param filename + * @param string + * @throws IOException + */ + public static void stringToFile(String filename, String string) throws IOException { + FileWriter out = new FileWriter(filename); + try { + out.write(string); + } finally { + out.close(); + } + } + + /** + * Computes the checksum of a file using the CRC32 checksum routine. + * The value of the checksum is returned. + * + * @param file the file to checksum, must not be null + * @return the checksum value or an exception is thrown. + */ + public static long checksumCrc32(File file) throws FileNotFoundException, IOException { + CRC32 checkSummer = new CRC32(); + CheckedInputStream cis = null; + + try { + cis = new CheckedInputStream( new FileInputStream(file), checkSummer); + byte[] buf = new byte[128]; + while(cis.read(buf) >= 0) { + // Just read for checksum to get calculated. + } + return checkSummer.getValue(); + } finally { + if (cis != null) { + try { + cis.close(); + } catch (IOException e) { + } + } + } + } + + /** + * Delete older files in a directory until only those matching the given + * constraints remain. + * + * @param minCount Always keep at least this many files. + * @param minAge Always keep files younger than this age. + * @return if any files were deleted. + */ + public static boolean deleteOlderFiles(File dir, int minCount, long minAge) { + if (minCount < 0 || minAge < 0) { + throw new IllegalArgumentException("Constraints must be positive or 0"); + } + + final File[] files = dir.listFiles(); + if (files == null) return false; + + // Sort with newest files first + Arrays.sort(files, new Comparator() { + @Override + public int compare(File lhs, File rhs) { + return (int) (rhs.lastModified() - lhs.lastModified()); + } + }); + + // Keep at least minCount files + boolean deleted = false; + for (int i = minCount; i < files.length; i++) { + final File file = files[i]; + + // Keep files newer than minAge + final long age = System.currentTimeMillis() - file.lastModified(); + if (age > minAge) { + if (file.delete()) { + Log.d(TAG, "Deleted old file " + file); + deleted = true; + } + } + } + return deleted; + } + + /** + * Test if a file lives under the given directory, either as a direct child + * or a distant grandchild. + *

+ * Both files must have been resolved using + * {@link File#getCanonicalFile()} to avoid symlink or path traversal + * attacks. + */ + public static boolean contains(File dir, File file) { + if (file == null) return false; + + String dirPath = dir.getAbsolutePath(); + String filePath = file.getAbsolutePath(); + + if (dirPath.equals(filePath)) { + return true; + } + + if (!dirPath.endsWith("/")) { + dirPath += "/"; + } + return filePath.startsWith(dirPath); + } + + public static boolean deleteContents(File dir) { + File[] files = dir.listFiles(); + boolean success = true; + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + success &= deleteContents(file); + } + if (!file.delete()) { + Log.w(TAG, "Failed to delete " + file); + success = false; + } + } + } + return success; + } + + private static boolean isValidExtFilenameChar(char c) { + switch (c) { + case '\0': + case '/': + return false; + default: + return true; + } + } + + /** + * Check if given filename is valid for an ext4 filesystem. + */ + public static boolean isValidExtFilename(String name) { + return (name != null) && name.equals(buildValidExtFilename(name)); + } + + /** + * Mutate the given filename to make it valid for an ext4 filesystem, + * replacing any invalid characters with "_". + */ + public static String buildValidExtFilename(String name) { + if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { + return "(invalid)"; + } + final StringBuilder res = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + final char c = name.charAt(i); + if (isValidExtFilenameChar(c)) { + res.append(c); + } else { + res.append('_'); + } + } + return res.toString(); + } + + private static boolean isValidFatFilenameChar(char c) { + if ((0x00 <= c && c <= 0x1f)) { + return false; + } + switch (c) { + case '"': + case '*': + case '/': + case ':': + case '<': + case '>': + case '?': + case '\\': + case '|': + case 0x7F: + return false; + default: + return true; + } + } + + /** + * Check if given filename is valid for a FAT filesystem. + */ + public static boolean isValidFatFilename(String name) { + return (name != null) && name.equals(buildValidFatFilename(name)); + } + + /** + * Mutate the given filename to make it valid for a FAT filesystem, + * replacing any invalid characters with "_". + */ + public static String buildValidFatFilename(String name) { + if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { + return "(invalid)"; + } + final StringBuilder res = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + final char c = name.charAt(i); + if (isValidFatFilenameChar(c)) { + res.append(c); + } else { + res.append('_'); + } + } + return res.toString(); + } + + public static String rewriteAfterRename(File beforeDir, File afterDir, String path) { + if (path == null) return null; + final File result = rewriteAfterRename(beforeDir, afterDir, new File(path)); + return (result != null) ? result.getAbsolutePath() : null; + } + + public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) { + if (paths == null) return null; + final String[] result = new String[paths.length]; + for (int i = 0; i < paths.length; i++) { + result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]); + } + return result; + } + + /** + * Given a path under the "before" directory, rewrite it to live under the + * "after" directory. For example, {@code /before/foo/bar.txt} would become + * {@code /after/foo/bar.txt}. + */ + public static File rewriteAfterRename(File beforeDir, File afterDir, File file) { + if (file == null) return null; + if (contains(beforeDir, file)) { + final String splice = file.getAbsolutePath().substring( + beforeDir.getAbsolutePath().length()); + return new File(afterDir, splice); + } + return null; + } +} diff --git a/src/main/java/android/os/FileUtilsTest.java b/src/main/java/android/os/FileUtilsTest.java new file mode 100644 index 0000000..5c9e813 --- /dev/null +++ b/src/main/java/android/os/FileUtilsTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2008 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 android.os; + +import static android.text.format.DateUtils.DAY_IN_MILLIS; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.WEEK_IN_MILLIS; + +import android.content.Context; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.MediumTest; + +import com.google.android.collect.Sets; + +import libcore.io.IoUtils; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.util.Arrays; +import java.util.HashSet; + +@MediumTest +public class FileUtilsTest extends AndroidTestCase { + private static final String TEST_DATA = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + private File mDir; + private File mTestFile; + private File mCopyFile; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mDir = getContext().getDir("testing", Context.MODE_PRIVATE); + mTestFile = new File(mDir, "test.file"); + mCopyFile = new File(mDir, "copy.file"); + } + + @Override + protected void tearDown() throws Exception { + IoUtils.deleteContents(mDir); + } + + // TODO: test setPermissions(), getPermissions() + + public void testCopyFile() throws Exception { + stageFile(mTestFile, TEST_DATA); + assertFalse(mCopyFile.exists()); + FileUtils.copyFile(mTestFile, mCopyFile); + assertTrue(mCopyFile.exists()); + assertEquals(TEST_DATA, FileUtils.readTextFile(mCopyFile, 0, null)); + } + + public void testCopyToFile() throws Exception { + final String s = "Foo Bar"; + assertFalse(mCopyFile.exists()); + FileUtils.copyToFile(new ByteArrayInputStream(s.getBytes()), mCopyFile); + assertTrue(mCopyFile.exists()); + assertEquals(s, FileUtils.readTextFile(mCopyFile, 0, null)); + } + + public void testIsFilenameSafe() throws Exception { + assertTrue(FileUtils.isFilenameSafe(new File("foobar"))); + assertTrue(FileUtils.isFilenameSafe(new File("a_b-c=d.e/0,1+23"))); + assertFalse(FileUtils.isFilenameSafe(new File("foo*bar"))); + assertFalse(FileUtils.isFilenameSafe(new File("foo\nbar"))); + } + + public void testReadTextFile() throws Exception { + stageFile(mTestFile, TEST_DATA); + + assertEquals(TEST_DATA, FileUtils.readTextFile(mTestFile, 0, null)); + + assertEquals("ABCDE", FileUtils.readTextFile(mTestFile, 5, null)); + assertEquals("ABCDE<>", FileUtils.readTextFile(mTestFile, 5, "<>")); + assertEquals(TEST_DATA.substring(0, 51) + "<>", + FileUtils.readTextFile(mTestFile, 51, "<>")); + assertEquals(TEST_DATA, FileUtils.readTextFile(mTestFile, 52, "<>")); + assertEquals(TEST_DATA, FileUtils.readTextFile(mTestFile, 100, "<>")); + + assertEquals("vwxyz", FileUtils.readTextFile(mTestFile, -5, null)); + assertEquals("<>vwxyz", FileUtils.readTextFile(mTestFile, -5, "<>")); + assertEquals("<>" + TEST_DATA.substring(1, 52), + FileUtils.readTextFile(mTestFile, -51, "<>")); + assertEquals(TEST_DATA, FileUtils.readTextFile(mTestFile, -52, "<>")); + assertEquals(TEST_DATA, FileUtils.readTextFile(mTestFile, -100, "<>")); + } + + public void testReadTextFileWithZeroLengthFile() throws Exception { + stageFile(mTestFile, TEST_DATA); + new FileOutputStream(mTestFile).close(); // Zero out the file + assertEquals("", FileUtils.readTextFile(mTestFile, 0, null)); + assertEquals("", FileUtils.readTextFile(mTestFile, 1, "<>")); + assertEquals("", FileUtils.readTextFile(mTestFile, 10, "<>")); + assertEquals("", FileUtils.readTextFile(mTestFile, -1, "<>")); + assertEquals("", FileUtils.readTextFile(mTestFile, -10, "<>")); + } + + public void testContains() throws Exception { + assertTrue(FileUtils.contains(new File("/"), new File("/moo.txt"))); + assertTrue(FileUtils.contains(new File("/"), new File("/"))); + + assertTrue(FileUtils.contains(new File("/sdcard"), new File("/sdcard"))); + assertTrue(FileUtils.contains(new File("/sdcard/"), new File("/sdcard/"))); + + assertTrue(FileUtils.contains(new File("/sdcard"), new File("/sdcard/moo.txt"))); + assertTrue(FileUtils.contains(new File("/sdcard/"), new File("/sdcard/moo.txt"))); + + assertFalse(FileUtils.contains(new File("/sdcard"), new File("/moo.txt"))); + assertFalse(FileUtils.contains(new File("/sdcard/"), new File("/moo.txt"))); + + assertFalse(FileUtils.contains(new File("/sdcard"), new File("/sdcard.txt"))); + assertFalse(FileUtils.contains(new File("/sdcard/"), new File("/sdcard.txt"))); + } + + public void testDeleteOlderEmptyDir() throws Exception { + FileUtils.deleteOlderFiles(mDir, 10, WEEK_IN_MILLIS); + assertDirContents(); + } + + public void testDeleteOlderTypical() throws Exception { + touch("file1", HOUR_IN_MILLIS); + touch("file2", 1 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + touch("file3", 2 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + touch("file4", 3 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + touch("file5", 4 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + assertTrue(FileUtils.deleteOlderFiles(mDir, 3, DAY_IN_MILLIS)); + assertDirContents("file1", "file2", "file3"); + } + + public void testDeleteOlderInFuture() throws Exception { + touch("file1", -HOUR_IN_MILLIS); + touch("file2", HOUR_IN_MILLIS); + touch("file3", WEEK_IN_MILLIS); + assertTrue(FileUtils.deleteOlderFiles(mDir, 0, DAY_IN_MILLIS)); + assertDirContents("file1", "file2"); + + touch("file1", -HOUR_IN_MILLIS); + touch("file2", HOUR_IN_MILLIS); + touch("file3", WEEK_IN_MILLIS); + assertTrue(FileUtils.deleteOlderFiles(mDir, 0, DAY_IN_MILLIS)); + assertDirContents("file1", "file2"); + } + + public void testDeleteOlderOnlyAge() throws Exception { + touch("file1", HOUR_IN_MILLIS); + touch("file2", 1 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + touch("file3", 2 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + touch("file4", 3 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + touch("file5", 4 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + assertTrue(FileUtils.deleteOlderFiles(mDir, 0, DAY_IN_MILLIS)); + assertFalse(FileUtils.deleteOlderFiles(mDir, 0, DAY_IN_MILLIS)); + assertDirContents("file1"); + } + + public void testDeleteOlderOnlyCount() throws Exception { + touch("file1", HOUR_IN_MILLIS); + touch("file2", 1 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + touch("file3", 2 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + touch("file4", 3 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + touch("file5", 4 * DAY_IN_MILLIS + HOUR_IN_MILLIS); + assertTrue(FileUtils.deleteOlderFiles(mDir, 2, 0)); + assertFalse(FileUtils.deleteOlderFiles(mDir, 2, 0)); + assertDirContents("file1", "file2"); + } + + public void testValidExtFilename() throws Exception { + assertTrue(FileUtils.isValidExtFilename("a")); + assertTrue(FileUtils.isValidExtFilename("foo.bar")); + assertTrue(FileUtils.isValidExtFilename("foo bar.baz")); + assertTrue(FileUtils.isValidExtFilename("foo.bar.baz")); + assertTrue(FileUtils.isValidExtFilename(".bar")); + assertTrue(FileUtils.isValidExtFilename("foo~!@#$%^&*()_[]{}+bar")); + + assertFalse(FileUtils.isValidExtFilename(null)); + assertFalse(FileUtils.isValidExtFilename(".")); + assertFalse(FileUtils.isValidExtFilename("../foo")); + assertFalse(FileUtils.isValidExtFilename("/foo")); + + assertEquals(".._foo", FileUtils.buildValidExtFilename("../foo")); + assertEquals("_foo", FileUtils.buildValidExtFilename("/foo")); + assertEquals("foo_bar", FileUtils.buildValidExtFilename("foo\0bar")); + assertEquals(".foo", FileUtils.buildValidExtFilename(".foo")); + assertEquals("foo.bar", FileUtils.buildValidExtFilename("foo.bar")); + } + + public void testValidFatFilename() throws Exception { + assertTrue(FileUtils.isValidFatFilename("a")); + assertTrue(FileUtils.isValidFatFilename("foo bar.baz")); + assertTrue(FileUtils.isValidFatFilename("foo.bar.baz")); + assertTrue(FileUtils.isValidFatFilename(".bar")); + assertTrue(FileUtils.isValidFatFilename("foo.bar")); + assertTrue(FileUtils.isValidFatFilename("foo bar")); + assertTrue(FileUtils.isValidFatFilename("foo+bar")); + assertTrue(FileUtils.isValidFatFilename("foo,bar")); + + assertFalse(FileUtils.isValidFatFilename("foo*bar")); + assertFalse(FileUtils.isValidFatFilename("foo?bar")); + assertFalse(FileUtils.isValidFatFilename("foo expectedSet = Sets.newHashSet(expected); + String[] actual = mDir.list(); + if (actual == null) actual = new String[0]; + + assertEquals( + "Expected " + Arrays.toString(expected) + " but actual " + Arrays.toString(actual), + expected.length, actual.length); + for (String actualFile : actual) { + assertTrue("Unexpected actual file " + actualFile, expectedSet.contains(actualFile)); + } + } +} diff --git a/src/main/java/android/os/Handler.java b/src/main/java/android/os/Handler.java new file mode 100644 index 0000000..878b7a0 --- /dev/null +++ b/src/main/java/android/os/Handler.java @@ -0,0 +1,798 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.util.Log; +import android.util.Printer; + +import java.lang.reflect.Modifier; + +/** + * A Handler allows you to send and process {@link Message} and Runnable + * objects associated with a thread's {@link MessageQueue}. Each Handler + * instance is associated with a single thread and that thread's message + * queue. When you create a new Handler, it is bound to the thread / + * message queue of the thread that is creating it -- from that point on, + * it will deliver messages and runnables to that message queue and execute + * them as they come out of the message queue. + * + *

There are two main uses for a Handler: (1) to schedule messages and + * runnables to be executed as some point in the future; and (2) to enqueue + * an action to be performed on a different thread than your own. + * + *

Scheduling messages is accomplished with the + * {@link #post}, {@link #postAtTime(Runnable, long)}, + * {@link #postDelayed}, {@link #sendEmptyMessage}, + * {@link #sendMessage}, {@link #sendMessageAtTime}, and + * {@link #sendMessageDelayed} methods. The post versions allow + * you to enqueue Runnable objects to be called by the message queue when + * they are received; the sendMessage versions allow you to enqueue + * a {@link Message} object containing a bundle of data that will be + * processed by the Handler's {@link #handleMessage} method (requiring that + * you implement a subclass of Handler). + * + *

When posting or sending to a Handler, you can either + * allow the item to be processed as soon as the message queue is ready + * to do so, or specify a delay before it gets processed or absolute time for + * it to be processed. The latter two allow you to implement timeouts, + * ticks, and other timing-based behavior. + * + *

When a + * process is created for your application, its main thread is dedicated to + * running a message queue that takes care of managing the top-level + * application objects (activities, broadcast receivers, etc) and any windows + * they create. You can create your own threads, and communicate back with + * the main application thread through a Handler. This is done by calling + * the same post or sendMessage methods as before, but from + * your new thread. The given Runnable or Message will then be scheduled + * in the Handler's message queue and processed when appropriate. + */ +public class Handler { + /* + * Set this flag to true to detect anonymous, local or member classes + * that extend this Handler class and that are not static. These kind + * of classes can potentially create leaks. + */ + private static final boolean FIND_POTENTIAL_LEAKS = false; + private static final String TAG = "Handler"; + + /** + * Callback interface you can use when instantiating a Handler to avoid + * having to implement your own subclass of Handler. + * + * @param msg A {@link android.os.Message Message} object + * @return True if no further handling is desired + */ + public interface Callback { + public boolean handleMessage(Message msg); + } + + /** + * Subclasses must implement this to receive messages. + */ + public void handleMessage(Message msg) { + } + + /** + * Handle system messages here. + */ + public void dispatchMessage(Message msg) { + if (msg.callback != null) { + handleCallback(msg); + } else { + if (mCallback != null) { + if (mCallback.handleMessage(msg)) { + return; + } + } + handleMessage(msg); + } + } + + /** + * Default constructor associates this handler with the {@link Looper} for the + * current thread. + * + * If this thread does not have a looper, this handler won't be able to receive messages + * so an exception is thrown. + */ + public Handler() { + this(null, false); + } + + /** + * Constructor associates this handler with the {@link Looper} for the + * current thread and takes a callback interface in which you can handle + * messages. + * + * If this thread does not have a looper, this handler won't be able to receive messages + * so an exception is thrown. + * + * @param callback The callback interface in which to handle messages, or null. + */ + public Handler(Callback callback) { + this(callback, false); + } + + /** + * Use the provided {@link Looper} instead of the default one. + * + * @param looper The looper, must not be null. + */ + public Handler(Looper looper) { + this(looper, null, false); + } + + /** + * Use the provided {@link Looper} instead of the default one and take a callback + * interface in which to handle messages. + * + * @param looper The looper, must not be null. + * @param callback The callback interface in which to handle messages, or null. + */ + public Handler(Looper looper, Callback callback) { + this(looper, callback, false); + } + + /** + * Use the {@link Looper} for the current thread + * and set whether the handler should be asynchronous. + * + * Handlers are synchronous by default unless this constructor is used to make + * one that is strictly asynchronous. + * + * Asynchronous messages represent interrupts or events that do not require global ordering + * with respect to synchronous messages. Asynchronous messages are not subject to + * the synchronization barriers introduced by {@link MessageQueue#enqueueSyncBarrier(long)}. + * + * @param async If true, the handler calls {@link Message#setAsynchronous(boolean)} for + * each {@link Message} that is sent to it or {@link Runnable} that is posted to it. + * + * @hide + */ + public Handler(boolean async) { + this(null, async); + } + + /** + * Use the {@link Looper} for the current thread with the specified callback interface + * and set whether the handler should be asynchronous. + * + * Handlers are synchronous by default unless this constructor is used to make + * one that is strictly asynchronous. + * + * Asynchronous messages represent interrupts or events that do not require global ordering + * with respect to synchronous messages. Asynchronous messages are not subject to + * the synchronization barriers introduced by {@link MessageQueue#enqueueSyncBarrier(long)}. + * + * @param callback The callback interface in which to handle messages, or null. + * @param async If true, the handler calls {@link Message#setAsynchronous(boolean)} for + * each {@link Message} that is sent to it or {@link Runnable} that is posted to it. + * + * @hide + */ + public Handler(Callback callback, boolean async) { + if (FIND_POTENTIAL_LEAKS) { + final Class klass = getClass(); + if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && + (klass.getModifiers() & Modifier.STATIC) == 0) { + Log.w(TAG, "The following Handler class should be static or leaks might occur: " + + klass.getCanonicalName()); + } + } + + mLooper = Looper.myLooper(); + if (mLooper == null) { + throw new RuntimeException( + "Can't create handler inside thread that has not called Looper.prepare()"); + } + mQueue = mLooper.mQueue; + mCallback = callback; + mAsynchronous = async; + } + + /** + * Use the provided {@link Looper} instead of the default one and take a callback + * interface in which to handle messages. Also set whether the handler + * should be asynchronous. + * + * Handlers are synchronous by default unless this constructor is used to make + * one that is strictly asynchronous. + * + * Asynchronous messages represent interrupts or events that do not require global ordering + * with respect to synchronous messages. Asynchronous messages are not subject to + * the synchronization barriers introduced by {@link MessageQueue#enqueueSyncBarrier(long)}. + * + * @param looper The looper, must not be null. + * @param callback The callback interface in which to handle messages, or null. + * @param async If true, the handler calls {@link Message#setAsynchronous(boolean)} for + * each {@link Message} that is sent to it or {@link Runnable} that is posted to it. + * + * @hide + */ + public Handler(Looper looper, Callback callback, boolean async) { + mLooper = looper; + mQueue = looper.mQueue; + mCallback = callback; + mAsynchronous = async; + } + + /** + * Returns a string representing the name of the specified message. + * The default implementation will either return the class name of the + * message callback if any, or the hexadecimal representation of the + * message "what" field. + * + * @param message The message whose name is being queried + */ + public String getMessageName(Message message) { + if (message.callback != null) { + return message.callback.getClass().getName(); + } + return "0x" + Integer.toHexString(message.what); + } + + /** + * Returns a new {@link android.os.Message Message} from the global message pool. More efficient than + * creating and allocating new instances. The retrieved message has its handler set to this instance (Message.target == this). + * If you don't want that facility, just call Message.obtain() instead. + */ + public final Message obtainMessage() + { + return Message.obtain(this); + } + + /** + * Same as {@link #obtainMessage()}, except that it also sets the what member of the returned Message. + * + * @param what Value to assign to the returned Message.what field. + * @return A Message from the global message pool. + */ + public final Message obtainMessage(int what) + { + return Message.obtain(this, what); + } + + /** + * + * Same as {@link #obtainMessage()}, except that it also sets the what and obj members + * of the returned Message. + * + * @param what Value to assign to the returned Message.what field. + * @param obj Value to assign to the returned Message.obj field. + * @return A Message from the global message pool. + */ + public final Message obtainMessage(int what, Object obj) + { + return Message.obtain(this, what, obj); + } + + /** + * + * Same as {@link #obtainMessage()}, except that it also sets the what, arg1 and arg2 members of the returned + * Message. + * @param what Value to assign to the returned Message.what field. + * @param arg1 Value to assign to the returned Message.arg1 field. + * @param arg2 Value to assign to the returned Message.arg2 field. + * @return A Message from the global message pool. + */ + public final Message obtainMessage(int what, int arg1, int arg2) + { + return Message.obtain(this, what, arg1, arg2); + } + + /** + * + * Same as {@link #obtainMessage()}, except that it also sets the what, obj, arg1,and arg2 values on the + * returned Message. + * @param what Value to assign to the returned Message.what field. + * @param arg1 Value to assign to the returned Message.arg1 field. + * @param arg2 Value to assign to the returned Message.arg2 field. + * @param obj Value to assign to the returned Message.obj field. + * @return A Message from the global message pool. + */ + public final Message obtainMessage(int what, int arg1, int arg2, Object obj) + { + return Message.obtain(this, what, arg1, arg2, obj); + } + + /** + * Causes the Runnable r to be added to the message queue. + * The runnable will be run on the thread to which this handler is + * attached. + * + * @param r The Runnable that will be executed. + * + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean post(Runnable r) + { + return sendMessageDelayed(getPostMessage(r), 0); + } + + /** + * Causes the Runnable r to be added to the message queue, to be run + * at a specific time given by uptimeMillis. + * The time-base is {@link android.os.SystemClock#uptimeMillis}. + * Time spent in deep sleep will add an additional delay to execution. + * The runnable will be run on the thread to which this handler is attached. + * + * @param r The Runnable that will be executed. + * @param uptimeMillis The absolute time at which the callback should run, + * using the {@link android.os.SystemClock#uptimeMillis} time-base. + * + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the Runnable will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public final boolean postAtTime(Runnable r, long uptimeMillis) + { + return sendMessageAtTime(getPostMessage(r), uptimeMillis); + } + + /** + * Causes the Runnable r to be added to the message queue, to be run + * at a specific time given by uptimeMillis. + * The time-base is {@link android.os.SystemClock#uptimeMillis}. + * Time spent in deep sleep will add an additional delay to execution. + * The runnable will be run on the thread to which this handler is attached. + * + * @param r The Runnable that will be executed. + * @param uptimeMillis The absolute time at which the callback should run, + * using the {@link android.os.SystemClock#uptimeMillis} time-base. + * + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the Runnable will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + * + * @see android.os.SystemClock#uptimeMillis + */ + public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) + { + return sendMessageAtTime(getPostMessage(r, token), uptimeMillis); + } + + /** + * Causes the Runnable r to be added to the message queue, to be run + * after the specified amount of time elapses. + * The runnable will be run on the thread to which this handler + * is attached. + * The time-base is {@link android.os.SystemClock#uptimeMillis}. + * Time spent in deep sleep will add an additional delay to execution. + * + * @param r The Runnable that will be executed. + * @param delayMillis The delay (in milliseconds) until the Runnable + * will be executed. + * + * @return Returns true if the Runnable was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the Runnable will be processed -- + * if the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public final boolean postDelayed(Runnable r, long delayMillis) + { + return sendMessageDelayed(getPostMessage(r), delayMillis); + } + + /** + * Posts a message to an object that implements Runnable. + * Causes the Runnable r to executed on the next iteration through the + * message queue. The runnable will be run on the thread to which this + * handler is attached. + * This method is only for use in very special circumstances -- it + * can easily starve the message queue, cause ordering problems, or have + * other unexpected side-effects. + * + * @param r The Runnable that will be executed. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean postAtFrontOfQueue(Runnable r) + { + return sendMessageAtFrontOfQueue(getPostMessage(r)); + } + + /** + * Runs the specified task synchronously. + *

+ * If the current thread is the same as the handler thread, then the runnable + * runs immediately without being enqueued. Otherwise, posts the runnable + * to the handler and waits for it to complete before returning. + *

+ * This method is dangerous! Improper use can result in deadlocks. + * Never call this method while any locks are held or use it in a + * possibly re-entrant manner. + *

+ * This method is occasionally useful in situations where a background thread + * must synchronously await completion of a task that must run on the + * handler's thread. However, this problem is often a symptom of bad design. + * Consider improving the design (if possible) before resorting to this method. + *

+ * One example of where you might want to use this method is when you just + * set up a Handler thread and need to perform some initialization steps on + * it before continuing execution. + *

+ * If timeout occurs then this method returns false but the runnable + * will remain posted on the handler and may already be in progress or + * complete at a later time. + *

+ * When using this method, be sure to use {@link Looper#quitSafely} when + * quitting the looper. Otherwise {@link #runWithScissors} may hang indefinitely. + * (TODO: We should fix this by making MessageQueue aware of blocking runnables.) + *

+ * + * @param r The Runnable that will be executed synchronously. + * @param timeout The timeout in milliseconds, or 0 to wait indefinitely. + * + * @return Returns true if the Runnable was successfully executed. + * Returns false on failure, usually because the + * looper processing the message queue is exiting. + * + * @hide This method is prone to abuse and should probably not be in the API. + * If we ever do make it part of the API, we might want to rename it to something + * less funny like runUnsafe(). + */ + public final boolean runWithScissors(final Runnable r, long timeout) { + if (r == null) { + throw new IllegalArgumentException("runnable must not be null"); + } + if (timeout < 0) { + throw new IllegalArgumentException("timeout must be non-negative"); + } + + if (Looper.myLooper() == mLooper) { + r.run(); + return true; + } + + BlockingRunnable br = new BlockingRunnable(r); + return br.postAndWait(this, timeout); + } + + /** + * Remove any pending posts of Runnable r that are in the message queue. + */ + public final void removeCallbacks(Runnable r) + { + mQueue.removeMessages(this, r, null); + } + + /** + * Remove any pending posts of Runnable r with Object + * token that are in the message queue. If token is null, + * all callbacks will be removed. + */ + public final void removeCallbacks(Runnable r, Object token) + { + mQueue.removeMessages(this, r, token); + } + + /** + * Pushes a message onto the end of the message queue after all pending messages + * before the current time. It will be received in {@link #handleMessage}, + * in the thread attached to this handler. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendMessage(Message msg) + { + return sendMessageDelayed(msg, 0); + } + + /** + * Sends a Message containing only the what value. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendEmptyMessage(int what) + { + return sendEmptyMessageDelayed(what, 0); + } + + /** + * Sends a Message containing only the what value, to be delivered + * after the specified amount of time elapses. + * @see #sendMessageDelayed(android.os.Message, long) + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { + Message msg = Message.obtain(); + msg.what = what; + return sendMessageDelayed(msg, delayMillis); + } + + /** + * Sends a Message containing only the what value, to be delivered + * at a specific time. + * @see #sendMessageAtTime(android.os.Message, long) + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + + public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) { + Message msg = Message.obtain(); + msg.what = what; + return sendMessageAtTime(msg, uptimeMillis); + } + + /** + * Enqueue a message into the message queue after all pending messages + * before (current time + delayMillis). You will receive it in + * {@link #handleMessage}, in the thread attached to this handler. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the message will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public final boolean sendMessageDelayed(Message msg, long delayMillis) + { + if (delayMillis < 0) { + delayMillis = 0; + } + return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); + } + + /** + * Enqueue a message into the message queue after all pending messages + * before the absolute time (in milliseconds) uptimeMillis. + * The time-base is {@link android.os.SystemClock#uptimeMillis}. + * Time spent in deep sleep will add an additional delay to execution. + * You will receive it in {@link #handleMessage}, in the thread attached + * to this handler. + * + * @param uptimeMillis The absolute time at which the message should be + * delivered, using the + * {@link android.os.SystemClock#uptimeMillis} time-base. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. Note that a + * result of true does not mean the message will be processed -- if + * the looper is quit before the delivery time of the message + * occurs then the message will be dropped. + */ + public boolean sendMessageAtTime(Message msg, long uptimeMillis) { + MessageQueue queue = mQueue; + if (queue == null) { + RuntimeException e = new RuntimeException( + this + " sendMessageAtTime() called with no mQueue"); + Log.w("Looper", e.getMessage(), e); + return false; + } + return enqueueMessage(queue, msg, uptimeMillis); + } + + /** + * Enqueue a message at the front of the message queue, to be processed on + * the next iteration of the message loop. You will receive it in + * {@link #handleMessage}, in the thread attached to this handler. + * This method is only for use in very special circumstances -- it + * can easily starve the message queue, cause ordering problems, or have + * other unexpected side-effects. + * + * @return Returns true if the message was successfully placed in to the + * message queue. Returns false on failure, usually because the + * looper processing the message queue is exiting. + */ + public final boolean sendMessageAtFrontOfQueue(Message msg) { + MessageQueue queue = mQueue; + if (queue == null) { + RuntimeException e = new RuntimeException( + this + " sendMessageAtTime() called with no mQueue"); + Log.w("Looper", e.getMessage(), e); + return false; + } + return enqueueMessage(queue, msg, 0); + } + + private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { + msg.target = this; + if (mAsynchronous) { + msg.setAsynchronous(true); + } + return queue.enqueueMessage(msg, uptimeMillis); + } + + /** + * Remove any pending posts of messages with code 'what' that are in the + * message queue. + */ + public final void removeMessages(int what) { + mQueue.removeMessages(this, what, null); + } + + /** + * Remove any pending posts of messages with code 'what' and whose obj is + * 'object' that are in the message queue. If object is null, + * all messages will be removed. + */ + public final void removeMessages(int what, Object object) { + mQueue.removeMessages(this, what, object); + } + + /** + * Remove any pending posts of callbacks and sent messages whose + * obj is token. If token is null, + * all callbacks and messages will be removed. + */ + public final void removeCallbacksAndMessages(Object token) { + mQueue.removeCallbacksAndMessages(this, token); + } + + /** + * Check if there are any pending posts of messages with code 'what' in + * the message queue. + */ + public final boolean hasMessages(int what) { + return mQueue.hasMessages(this, what, null); + } + + /** + * Check if there are any pending posts of messages with code 'what' and + * whose obj is 'object' in the message queue. + */ + public final boolean hasMessages(int what, Object object) { + return mQueue.hasMessages(this, what, object); + } + + /** + * Check if there are any pending posts of messages with callback r in + * the message queue. + * + * @hide + */ + public final boolean hasCallbacks(Runnable r) { + return mQueue.hasMessages(this, r, null); + } + + // if we can get rid of this method, the handler need not remember its loop + // we could instead export a getMessageQueue() method... + public final Looper getLooper() { + return mLooper; + } + + public final void dump(Printer pw, String prefix) { + pw.println(prefix + this + " @ " + SystemClock.uptimeMillis()); + if (mLooper == null) { + pw.println(prefix + "looper uninitialized"); + } else { + mLooper.dump(pw, prefix + " "); + } + } + + @Override + public String toString() { + return "Handler (" + getClass().getName() + ") {" + + Integer.toHexString(System.identityHashCode(this)) + + "}"; + } + + final IMessenger getIMessenger() { + synchronized (mQueue) { + if (mMessenger != null) { + return mMessenger; + } + mMessenger = new MessengerImpl(); + return mMessenger; + } + } + + private final class MessengerImpl extends IMessenger.Stub { + public void send(Message msg) { + msg.sendingUid = Binder.getCallingUid(); + Handler.this.sendMessage(msg); + } + } + + private static Message getPostMessage(Runnable r) { + Message m = Message.obtain(); + m.callback = r; + return m; + } + + private static Message getPostMessage(Runnable r, Object token) { + Message m = Message.obtain(); + m.obj = token; + m.callback = r; + return m; + } + + private static void handleCallback(Message message) { + message.callback.run(); + } + + final MessageQueue mQueue; + final Looper mLooper; + final Callback mCallback; + final boolean mAsynchronous; + IMessenger mMessenger; + + private static final class BlockingRunnable implements Runnable { + private final Runnable mTask; + private boolean mDone; + + public BlockingRunnable(Runnable task) { + mTask = task; + } + + @Override + public void run() { + try { + mTask.run(); + } finally { + synchronized (this) { + mDone = true; + notifyAll(); + } + } + } + + public boolean postAndWait(Handler handler, long timeout) { + if (!handler.post(this)) { + return false; + } + + synchronized (this) { + if (timeout > 0) { + final long expirationTime = SystemClock.uptimeMillis() + timeout; + while (!mDone) { + long delay = expirationTime - SystemClock.uptimeMillis(); + if (delay <= 0) { + return false; // timeout + } + try { + wait(delay); + } catch (InterruptedException ex) { + } + } + } else { + while (!mDone) { + try { + wait(); + } catch (InterruptedException ex) { + } + } + } + } + return true; + } + } +} diff --git a/src/main/java/android/os/HandlerTester.java b/src/main/java/android/os/HandlerTester.java new file mode 100644 index 0000000..a216a0b --- /dev/null +++ b/src/main/java/android/os/HandlerTester.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +public abstract class HandlerTester extends Thread { + public abstract void go(); + public abstract void handleMessage(Message msg); + + public HandlerTester() { + } + + public void doTest(long timeout) { + start(); + + synchronized (this) { + try { + wait(timeout); + quit(); + } + catch (InterruptedException e) { + } + } + + if (!mDone) { + throw new RuntimeException("test timed out"); + } + if (!mSuccess) { + throw new RuntimeException("test failed"); + } + } + + public void success() { + mDone = true; + mSuccess = true; + } + + public void failure() { + mDone = true; + mSuccess = false; + } + + public void run() { + Looper.prepare(); + mLooper = Looper.myLooper(); + go(); + Looper.loop(); + } + + protected class H extends Handler { + public void handleMessage(Message msg) { + synchronized (HandlerTester.this) { + // Call into them with our monitor locked, so they don't have + // to deal with other races. + HandlerTester.this.handleMessage(msg); + if (mDone) { + HandlerTester.this.notify(); + quit(); + } + } + } + } + + private void quit() { + mLooper.quit(); + } + + private boolean mDone = false; + private boolean mSuccess = false; + private Looper mLooper; +} + diff --git a/src/main/java/android/os/HandlerThread.java b/src/main/java/android/os/HandlerThread.java new file mode 100644 index 0000000..2904105 --- /dev/null +++ b/src/main/java/android/os/HandlerThread.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2006 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 android.os; + +/** + * Handy class for starting a new thread that has a looper. The looper can then be + * used to create handler classes. Note that start() must still be called. + */ +public class HandlerThread extends Thread { + int mPriority; + int mTid = -1; + Looper mLooper; + + public HandlerThread(String name) { + super(name); + mPriority = Process.THREAD_PRIORITY_DEFAULT; + } + + /** + * Constructs a HandlerThread. + * @param name + * @param priority The priority to run the thread at. The value supplied must be from + * {@link android.os.Process} and not from java.lang.Thread. + */ + public HandlerThread(String name, int priority) { + super(name); + mPriority = priority; + } + + /** + * Call back method that can be explicitly overridden if needed to execute some + * setup before Looper loops. + */ + protected void onLooperPrepared() { + } + + @Override + public void run() { + mTid = Process.myTid(); + Looper.prepare(); + synchronized (this) { + mLooper = Looper.myLooper(); + notifyAll(); + } + Process.setThreadPriority(mPriority); + onLooperPrepared(); + Looper.loop(); + mTid = -1; + } + + /** + * This method returns the Looper associated with this thread. If this thread not been started + * or for any reason is isAlive() returns false, this method will return null. If this thread + * has been started, this method will block until the looper has been initialized. + * @return The looper. + */ + public Looper getLooper() { + if (!isAlive()) { + return null; + } + + // If the thread has been started, wait until the looper has been created. + synchronized (this) { + while (isAlive() && mLooper == null) { + try { + wait(); + } catch (InterruptedException e) { + } + } + } + return mLooper; + } + + /** + * Quits the handler thread's looper. + *

+ * Causes the handler thread's looper to terminate without processing any + * more messages in the message queue. + *

+ * Any attempt to post messages to the queue after the looper is asked to quit will fail. + * For example, the {@link Handler#sendMessage(Message)} method will return false. + *

+ * Using this method may be unsafe because some messages may not be delivered + * before the looper terminates. Consider using {@link #quitSafely} instead to ensure + * that all pending work is completed in an orderly manner. + *

+ * + * @return True if the looper looper has been asked to quit or false if the + * thread had not yet started running. + * + * @see #quitSafely + */ + public boolean quit() { + Looper looper = getLooper(); + if (looper != null) { + looper.quit(); + return true; + } + return false; + } + + /** + * Quits the handler thread's looper safely. + *

+ * Causes the handler thread's looper to terminate as soon as all remaining messages + * in the message queue that are already due to be delivered have been handled. + * Pending delayed messages with due times in the future will not be delivered. + *

+ * Any attempt to post messages to the queue after the looper is asked to quit will fail. + * For example, the {@link Handler#sendMessage(Message)} method will return false. + *

+ * If the thread has not been started or has finished (that is if + * {@link #getLooper} returns null), then false is returned. + * Otherwise the looper is asked to quit and true is returned. + *

+ * + * @return True if the looper looper has been asked to quit or false if the + * thread had not yet started running. + */ + public boolean quitSafely() { + Looper looper = getLooper(); + if (looper != null) { + looper.quitSafely(); + return true; + } + return false; + } + + /** + * Returns the identifier of this thread. See Process.myTid(). + */ + public int getThreadId() { + return mTid; + } +} diff --git a/src/main/java/android/os/HandlerThreadTest.java b/src/main/java/android/os/HandlerThreadTest.java new file mode 100644 index 0000000..9772aa4 --- /dev/null +++ b/src/main/java/android/os/HandlerThreadTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2006 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 android.os; + +import junit.framework.TestCase; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.os.Process; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; + +public class HandlerThreadTest extends TestCase { + private static final int TEST_WHAT = 1; + + private boolean mGotMessage = false; + private int mGotMessageWhat = -1; + private volatile boolean mDidSetup = false; + private volatile int mLooperTid = -1; + + @MediumTest + public void testHandlerThread() throws Exception { + HandlerThread th1 = new HandlerThread("HandlerThreadTest") { + protected void onLooperPrepared() { + synchronized (HandlerThreadTest.this) { + mDidSetup = true; + mLooperTid = Process.myTid(); + HandlerThreadTest.this.notify(); + } + } + }; + + assertFalse(th1.isAlive()); + assertNull(th1.getLooper()); + + th1.start(); + + assertTrue(th1.isAlive()); + assertNotNull(th1.getLooper()); + + // The call to getLooper() internally blocks until the looper is + // available, but will call onLooperPrepared() after that. So we + // need to block here to wait for our onLooperPrepared() to complete + // and fill in the values we expect. + synchronized (this) { + while (!mDidSetup) { + try { + wait(); + } catch (InterruptedException e) { + } + } + } + + // Make sure that the process was set. + assertNotSame(-1, mLooperTid); + // Make sure that the onLooperPrepared() was called on a different thread. + assertNotSame(Process.myTid(), mLooperTid); + + final Handler h1 = new Handler(th1.getLooper()) { + public void handleMessage(Message msg) { + assertEquals(TEST_WHAT, msg.what); + // Ensure that we are running on the same thread in which the looper was setup on. + assertEquals(mLooperTid, Process.myTid()); + + mGotMessageWhat = msg.what; + mGotMessage = true; + synchronized(this) { + notifyAll(); + } + } + }; + + Message msg = h1.obtainMessage(TEST_WHAT); + + synchronized (h1) { + // wait until we have the lock before sending the message. + h1.sendMessage(msg); + try { + // wait for the message to be handled + h1.wait(); + } catch (InterruptedException e) { + } + } + + assertTrue(mGotMessage); + assertEquals(TEST_WHAT, mGotMessageWhat); + } +} diff --git a/src/main/java/android/os/HandlerThread_Delegate.java b/src/main/java/android/os/HandlerThread_Delegate.java new file mode 100644 index 0000000..afbe97c --- /dev/null +++ b/src/main/java/android/os/HandlerThread_Delegate.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2011 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 android.os; + +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.impl.RenderAction; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Delegate overriding selected methods of android.os.HandlerThread + * + * Through the layoutlib_create tool, selected methods of Handler have been replaced + * by calls to methods of the same name in this delegate class. + * + * + */ +public class HandlerThread_Delegate { + + private static Map> sThreads = + new HashMap>(); + + public static void cleanUp(BridgeContext context) { + List list = sThreads.get(context); + if (list != null) { + for (HandlerThread thread : list) { + thread.quit(); + } + + list.clear(); + sThreads.remove(context); + } + } + + // -------- Delegate methods + + @LayoutlibDelegate + /*package*/ static void run(HandlerThread theThread) { + // record the thread so that it can be quit() on clean up. + BridgeContext context = RenderAction.getCurrentContext(); + List list = sThreads.get(context); + if (list == null) { + list = new ArrayList(); + sThreads.put(context, list); + } + + list.add(theThread); + + // ---- START DEFAULT IMPLEMENTATION. + + theThread.mTid = Process.myTid(); + Looper.prepare(); + synchronized (theThread) { + theThread.mLooper = Looper.myLooper(); + theThread.notifyAll(); + } + Process.setThreadPriority(theThread.mPriority); + theThread.onLooperPrepared(); + Looper.loop(); + theThread.mTid = -1; + } +} diff --git a/src/main/java/android/os/Handler_Delegate.java b/src/main/java/android/os/Handler_Delegate.java new file mode 100644 index 0000000..2152c8a --- /dev/null +++ b/src/main/java/android/os/Handler_Delegate.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 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 android.os; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + + +/** + * Delegate overriding selected methods of android.os.Handler + * + * Through the layoutlib_create tool, selected methods of Handler have been replaced + * by calls to methods of the same name in this delegate class. + * + * + */ +public class Handler_Delegate { + + // -------- Delegate methods + + @LayoutlibDelegate + /*package*/ static boolean sendMessageAtTime(Handler handler, Message msg, long uptimeMillis) { + // get the callback + IHandlerCallback callback = sCallbacks.get(); + if (callback != null) { + callback.sendMessageAtTime(handler, msg, uptimeMillis); + } + return true; + } + + // -------- Delegate implementation + + public interface IHandlerCallback { + void sendMessageAtTime(Handler handler, Message msg, long uptimeMillis); + } + + private final static ThreadLocal sCallbacks = + new ThreadLocal(); + + public static void setCallback(IHandlerCallback callback) { + sCallbacks.set(callback); + } + +} diff --git a/src/main/java/android/os/IBinder.java b/src/main/java/android/os/IBinder.java new file mode 100644 index 0000000..73a0f65 --- /dev/null +++ b/src/main/java/android/os/IBinder.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2006 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 android.os; + +import java.io.FileDescriptor; + +/** + * Base interface for a remotable object, the core part of a lightweight + * remote procedure call mechanism designed for high performance when + * performing in-process and cross-process calls. This + * interface describes the abstract protocol for interacting with a + * remotable object. Do not implement this interface directly, instead + * extend from {@link Binder}. + * + *

The key IBinder API is {@link #transact transact()} matched by + * {@link Binder#onTransact Binder.onTransact()}. These + * methods allow you to send a call to an IBinder object and receive a + * call coming in to a Binder object, respectively. This transaction API + * is synchronous, such that a call to {@link #transact transact()} does not + * return until the target has returned from + * {@link Binder#onTransact Binder.onTransact()}; this is the + * expected behavior when calling an object that exists in the local + * process, and the underlying inter-process communication (IPC) mechanism + * ensures that these same semantics apply when going across processes. + * + *

The data sent through transact() is a {@link Parcel}, a generic buffer + * of data that also maintains some meta-data about its contents. The meta + * data is used to manage IBinder object references in the buffer, so that those + * references can be maintained as the buffer moves across processes. This + * mechanism ensures that when an IBinder is written into a Parcel and sent to + * another process, if that other process sends a reference to that same IBinder + * back to the original process, then the original process will receive the + * same IBinder object back. These semantics allow IBinder/Binder objects to + * be used as a unique identity (to serve as a token or for other purposes) + * that can be managed across processes. + * + *

The system maintains a pool of transaction threads in each process that + * it runs in. These threads are used to dispatch all + * IPCs coming in from other processes. For example, when an IPC is made from + * process A to process B, the calling thread in A blocks in transact() as + * it sends the transaction to process B. The next available pool thread in + * B receives the incoming transaction, calls Binder.onTransact() on the target + * object, and replies with the result Parcel. Upon receiving its result, the + * thread in process A returns to allow its execution to continue. In effect, + * other processes appear to use as additional threads that you did not create + * executing in your own process. + * + *

The Binder system also supports recursion across processes. For example + * if process A performs a transaction to process B, and process B while + * handling that transaction calls transact() on an IBinder that is implemented + * in A, then the thread in A that is currently waiting for the original + * transaction to finish will take care of calling Binder.onTransact() on the + * object being called by B. This ensures that the recursion semantics when + * calling remote binder object are the same as when calling local objects. + * + *

When working with remote objects, you often want to find out when they + * are no longer valid. There are three ways this can be determined: + *

    + *
  • The {@link #transact transact()} method will throw a + * {@link RemoteException} exception if you try to call it on an IBinder + * whose process no longer exists. + *
  • The {@link #pingBinder()} method can be called, and will return false + * if the remote process no longer exists. + *
  • The {@link #linkToDeath linkToDeath()} method can be used to register + * a {@link DeathRecipient} with the IBinder, which will be called when its + * containing process goes away. + *
+ * + * @see Binder + */ +public interface IBinder { + /** + * The first transaction code available for user commands. + */ + int FIRST_CALL_TRANSACTION = 0x00000001; + /** + * The last transaction code available for user commands. + */ + int LAST_CALL_TRANSACTION = 0x00ffffff; + + /** + * IBinder protocol transaction code: pingBinder(). + */ + int PING_TRANSACTION = ('_'<<24)|('P'<<16)|('N'<<8)|'G'; + + /** + * IBinder protocol transaction code: dump internal state. + */ + int DUMP_TRANSACTION = ('_'<<24)|('D'<<16)|('M'<<8)|'P'; + + /** + * IBinder protocol transaction code: interrogate the recipient side + * of the transaction for its canonical interface descriptor. + */ + int INTERFACE_TRANSACTION = ('_'<<24)|('N'<<16)|('T'<<8)|'F'; + + /** + * IBinder protocol transaction code: send a tweet to the target + * object. The data in the parcel is intended to be delivered to + * a shared messaging service associated with the object; it can be + * anything, as long as it is not more than 130 UTF-8 characters to + * conservatively fit within common messaging services. As part of + * {@link Build.VERSION_CODES#HONEYCOMB_MR2}, all Binder objects are + * expected to support this protocol for fully integrated tweeting + * across the platform. To support older code, the default implementation + * logs the tweet to the main log as a simple emulation of broadcasting + * it publicly over the Internet. + * + *

Also, upon completing the dispatch, the object must make a cup + * of tea, return it to the caller, and exclaim "jolly good message + * old boy!". + */ + int TWEET_TRANSACTION = ('_'<<24)|('T'<<16)|('W'<<8)|'T'; + + /** + * IBinder protocol transaction code: tell an app asynchronously that the + * caller likes it. The app is responsible for incrementing and maintaining + * its own like counter, and may display this value to the user to indicate the + * quality of the app. This is an optional command that applications do not + * need to handle, so the default implementation is to do nothing. + * + *

There is no response returned and nothing about the + * system will be functionally affected by it, but it will improve the + * app's self-esteem. + */ + int LIKE_TRANSACTION = ('_'<<24)|('L'<<16)|('I'<<8)|'K'; + + /** @hide */ + int SYSPROPS_TRANSACTION = ('_'<<24)|('S'<<16)|('P'<<8)|'R'; + + /** + * Flag to {@link #transact}: this is a one-way call, meaning that the + * caller returns immediately, without waiting for a result from the + * callee. Applies only if the caller and callee are in different + * processes. + */ + int FLAG_ONEWAY = 0x00000001; + + /** + * Get the canonical name of the interface supported by this binder. + */ + public String getInterfaceDescriptor() throws RemoteException; + + /** + * Check to see if the object still exists. + * + * @return Returns false if the + * hosting process is gone, otherwise the result (always by default + * true) returned by the pingBinder() implementation on the other + * side. + */ + public boolean pingBinder(); + + /** + * Check to see if the process that the binder is in is still alive. + * + * @return false if the process is not alive. Note that if it returns + * true, the process may have died while the call is returning. + */ + public boolean isBinderAlive(); + + /** + * Attempt to retrieve a local implementation of an interface + * for this Binder object. If null is returned, you will need + * to instantiate a proxy class to marshall calls through + * the transact() method. + */ + public IInterface queryLocalInterface(String descriptor); + + /** + * Print the object's state into the given stream. + * + * @param fd The raw file descriptor that the dump is being sent to. + * @param args additional arguments to the dump request. + */ + public void dump(FileDescriptor fd, String[] args) throws RemoteException; + + /** + * Like {@link #dump(FileDescriptor, String[])} but always executes + * asynchronously. If the object is local, a new thread is created + * to perform the dump. + * + * @param fd The raw file descriptor that the dump is being sent to. + * @param args additional arguments to the dump request. + */ + public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException; + + /** + * Perform a generic operation with the object. + * + * @param code The action to perform. This should + * be a number between {@link #FIRST_CALL_TRANSACTION} and + * {@link #LAST_CALL_TRANSACTION}. + * @param data Marshalled data to send to the target. Must not be null. + * If you are not sending any data, you must create an empty Parcel + * that is given here. + * @param reply Marshalled data to be received from the target. May be + * null if you are not interested in the return value. + * @param flags Additional operation flags. Either 0 for a normal + * RPC, or {@link #FLAG_ONEWAY} for a one-way RPC. + */ + public boolean transact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException; + + /** + * Interface for receiving a callback when the process hosting an IBinder + * has gone away. + * + * @see #linkToDeath + */ + public interface DeathRecipient { + public void binderDied(); + } + + /** + * Register the recipient for a notification if this binder + * goes away. If this binder object unexpectedly goes away + * (typically because its hosting process has been killed), + * then the given {@link DeathRecipient}'s + * {@link DeathRecipient#binderDied DeathRecipient.binderDied()} method + * will be called. + * + *

You will only receive death notifications for remote binders, + * as local binders by definition can't die without you dying as well. + * + * @throws RemoteException if the target IBinder's + * process has already died. + * + * @see #unlinkToDeath + */ + public void linkToDeath(DeathRecipient recipient, int flags) + throws RemoteException; + + /** + * Remove a previously registered death notification. + * The recipient will no longer be called if this object + * dies. + * + * @return {@code true} if the recipient is successfully + * unlinked, assuring you that its + * {@link DeathRecipient#binderDied DeathRecipient.binderDied()} method + * will not be called; {@code false} if the target IBinder has already + * died, meaning the method has been (or soon will be) called. + * + * @throws java.util.NoSuchElementException if the given + * recipient has not been registered with the IBinder, and + * the IBinder is still alive. Note that if the recipient + * was never registered, but the IBinder has already died, then this + * exception will not be thrown, and you will receive a false + * return value instead. + */ + public boolean unlinkToDeath(DeathRecipient recipient, int flags); +} diff --git a/src/main/java/android/os/IInterface.java b/src/main/java/android/os/IInterface.java new file mode 100644 index 0000000..2a2605a --- /dev/null +++ b/src/main/java/android/os/IInterface.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2006 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 android.os; + +/** + * Base class for Binder interfaces. When defining a new interface, + * you must derive it from IInterface. + */ +public interface IInterface +{ + /** + * Retrieve the Binder object associated with this interface. + * You must use this instead of a plain cast, so that proxy objects + * can return the correct result. + */ + public IBinder asBinder(); +} diff --git a/src/main/java/android/os/IServiceManager.java b/src/main/java/android/os/IServiceManager.java new file mode 100644 index 0000000..7b11c28 --- /dev/null +++ b/src/main/java/android/os/IServiceManager.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2006 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 android.os; + +/** + * Basic interface for finding and publishing system services. + * + * An implementation of this interface is usually published as the + * global context object, which can be retrieved via + * BinderNative.getContextObject(). An easy way to retrieve this + * is with the static method BnServiceManager.getDefault(). + * + * @hide + */ +public interface IServiceManager extends IInterface +{ + /** + * Retrieve an existing service called @a name from the + * service manager. Blocks for a few seconds waiting for it to be + * published if it does not already exist. + */ + public IBinder getService(String name) throws RemoteException; + + /** + * Retrieve an existing service called @a name from the + * service manager. Non-blocking. + */ + public IBinder checkService(String name) throws RemoteException; + + /** + * Place a new @a service called @a name into the service + * manager. + */ + public void addService(String name, IBinder service, boolean allowIsolated) + throws RemoteException; + + /** + * Return a list of all currently running services. + */ + public String[] listServices() throws RemoteException; + + /** + * Assign a permission controller to the service manager. After set, this + * interface is checked before any services are added. + */ + public void setPermissionController(IPermissionController controller) + throws RemoteException; + + static final String descriptor = "android.os.IServiceManager"; + + int GET_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION; + int CHECK_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+1; + int ADD_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+2; + int LIST_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+3; + int CHECK_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+4; + int SET_PERMISSION_CONTROLLER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+5; +} diff --git a/src/main/java/android/os/IdleHandlerTest.java b/src/main/java/android/os/IdleHandlerTest.java new file mode 100644 index 0000000..6c0a862 --- /dev/null +++ b/src/main/java/android/os/IdleHandlerTest.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.MessageQueue.IdleHandler; +import android.test.suitebuilder.annotation.MediumTest; +import junit.framework.TestCase; + +public class IdleHandlerTest extends TestCase { + + private static class BaseTestHandler extends TestHandlerThread { + Handler mHandler; + + public BaseTestHandler() { + } + + public void go() { + mHandler = new Handler() { + public void handleMessage(Message msg) { + BaseTestHandler.this.handleMessage(msg); + } + }; + } + + public void addIdleHandler() { + Looper.myQueue().addIdleHandler(new IdleHandler() { + public boolean queueIdle() { + return BaseTestHandler.this.queueIdle(); + } + }); + } + + public void handleMessage(Message msg) { + } + + public boolean queueIdle() { + return false; + } + } + + @MediumTest + public void testOneShotFirst() throws Exception { + TestHandlerThread tester = new BaseTestHandler() { + int mCount; + + public void go() { + super.go(); + mCount = 0; + mHandler.sendMessageDelayed(mHandler.obtainMessage(0), 100); + addIdleHandler(); + } + + public void handleMessage(Message msg) { + if (msg.what == 0) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(1), 100); + } else if (msg.what == 1) { + if (mCount == 1) { + success(); + } else { + failure(new RuntimeException( + "Idle handler called " + mCount + " times")); + } + } + } + + public boolean queueIdle() { + mCount++; + return false; + } + }; + + tester.doTest(1000); + } + + @MediumTest + public void testOneShotLater() throws Exception { + TestHandlerThread tester = new BaseTestHandler() { + int mCount; + + public void go() { + super.go(); + mCount = 0; + mHandler.sendMessage(mHandler.obtainMessage(0)); + } + + public void handleMessage(Message msg) { + if (msg.what == 0) { + addIdleHandler(); + mHandler.sendMessageDelayed(mHandler.obtainMessage(1), 100); + } else if (msg.what == 1) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(2), 100); + } else if (msg.what == 2) { + if (mCount == 1) { + success(); + } else { + failure(new RuntimeException( + "Idle handler called " + mCount + " times")); + } + } + } + + public boolean queueIdle() { + mCount++; + return false; + } + }; + + tester.doTest(1000); + } + + + @MediumTest + public void testRepeatedFirst() throws Exception { + TestHandlerThread tester = new BaseTestHandler() { + int mCount; + + public void go() { + super.go(); + mCount = 0; + mHandler.sendMessageDelayed(mHandler.obtainMessage(0), 100); + addIdleHandler(); + } + + public void handleMessage(Message msg) { + if (msg.what == 0) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(1), 100); + } else if (msg.what == 1) { + if (mCount == 2) { + success(); + } else { + failure(new RuntimeException( + "Idle handler called " + mCount + " times")); + } + } + } + + public boolean queueIdle() { + mCount++; + return true; + } + }; + + tester.doTest(1000); + } + + @MediumTest + public void testRepeatedLater() throws Exception { + TestHandlerThread tester = new BaseTestHandler() { + int mCount; + + public void go() { + super.go(); + mCount = 0; + mHandler.sendMessage(mHandler.obtainMessage(0)); + } + + public void handleMessage(Message msg) { + if (msg.what == 0) { + addIdleHandler(); + mHandler.sendMessageDelayed(mHandler.obtainMessage(1), 100); + } else if (msg.what == 1) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(2), 100); + } else if (msg.what == 2) { + if (mCount == 2) { + success(); + } else { + failure(new RuntimeException( + "Idle handler called " + mCount + " times")); + } + } + } + + public boolean queueIdle() { + mCount++; + return true; + } + }; + + tester.doTest(1000); + } +} + diff --git a/src/main/java/android/os/InputMethodSubtypeArrayTest.java b/src/main/java/android/os/InputMethodSubtypeArrayTest.java new file mode 100644 index 0000000..1e0a919 --- /dev/null +++ b/src/main/java/android/os/InputMethodSubtypeArrayTest.java @@ -0,0 +1,77 @@ +/* + * 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 android.os; + +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.inputmethod.InputMethodSubtype; +import android.view.inputmethod.InputMethodSubtypeArray; +import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; + +import java.util.ArrayList; + +public class InputMethodSubtypeArrayTest extends InstrumentationTestCase { + @SmallTest + public void testInstanciate() throws Exception { + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummySubtype(0, "en_US")); + subtypes.add(createDummySubtype(1, "en_US")); + subtypes.add(createDummySubtype(2, "ja_JP")); + + final InputMethodSubtypeArray array = new InputMethodSubtypeArray(subtypes); + assertEquals(subtypes.size(), array.getCount()); + assertEquals(subtypes.get(0), array.get(0)); + assertEquals(subtypes.get(1), array.get(1)); + assertEquals(subtypes.get(2), array.get(2)); + + final InputMethodSubtypeArray clonedArray = cloneViaParcel(array); + assertEquals(subtypes.size(), clonedArray.getCount()); + assertEquals(subtypes.get(0), clonedArray.get(0)); + assertEquals(subtypes.get(1), clonedArray.get(1)); + assertEquals(subtypes.get(2), clonedArray.get(2)); + + final InputMethodSubtypeArray clonedClonedArray = cloneViaParcel(clonedArray); + assertEquals(clonedArray.getCount(), clonedClonedArray.getCount()); + assertEquals(clonedArray.get(0), clonedClonedArray.get(0)); + assertEquals(clonedArray.get(1), clonedClonedArray.get(1)); + assertEquals(clonedArray.get(2), clonedClonedArray.get(2)); + } + + InputMethodSubtypeArray cloneViaParcel(final InputMethodSubtypeArray original) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + original.writeToParcel(parcel); + parcel.setDataPosition(0); + return new InputMethodSubtypeArray(parcel); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } + + private static InputMethodSubtype createDummySubtype(final int id, final String locale) { + final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder(); + return builder.setSubtypeNameResId(0) + .setSubtypeIconResId(0) + .setSubtypeId(id) + .setSubtypeLocale(locale) + .setIsAsciiCapable(true) + .build(); + } +} diff --git a/src/main/java/android/os/InputMethodSubtypeSwitchingControllerTest.java b/src/main/java/android/os/InputMethodSubtypeSwitchingControllerTest.java new file mode 100644 index 0000000..3a598f2 --- /dev/null +++ b/src/main/java/android/os/InputMethodSubtypeSwitchingControllerTest.java @@ -0,0 +1,327 @@ +/* + * 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 android.os; + +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; +import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; + +import com.android.internal.inputmethod.InputMethodSubtypeSwitchingController.ControllerImpl; +import com.android.internal.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; +import com.android.internal.inputmethod.InputMethodUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class InputMethodSubtypeSwitchingControllerTest extends InstrumentationTestCase { + private static final String DUMMY_PACKAGE_NAME = "dymmy package name"; + private static final String DUMMY_SETTING_ACTIVITY_NAME = ""; + private static final boolean DUMMY_IS_AUX_IME = false; + private static final boolean DUMMY_FORCE_DEFAULT = false; + private static final int DUMMY_IS_DEFAULT_RES_ID = 0; + private static final String SYSTEM_LOCALE = "en_US"; + private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; + + private static InputMethodSubtype createDummySubtype(final String locale) { + final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder(); + return builder.setSubtypeNameResId(0) + .setSubtypeIconResId(0) + .setSubtypeLocale(locale) + .setIsAsciiCapable(true) + .build(); + } + + private static void addDummyImeSubtypeListItems(List items, + String imeName, String imeLabel, List subtypeLocales, + boolean supportsSwitchingToNextInputMethod) { + final ResolveInfo ri = new ResolveInfo(); + final ServiceInfo si = new ServiceInfo(); + final ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = DUMMY_PACKAGE_NAME; + ai.enabled = true; + si.applicationInfo = ai; + si.enabled = true; + si.packageName = DUMMY_PACKAGE_NAME; + si.name = imeName; + si.exported = true; + si.nonLocalizedLabel = imeLabel; + ri.serviceInfo = si; + List subtypes = null; + if (subtypeLocales != null) { + subtypes = new ArrayList(); + for (String subtypeLocale : subtypeLocales) { + subtypes.add(createDummySubtype(subtypeLocale)); + } + } + final InputMethodInfo imi = new InputMethodInfo(ri, DUMMY_IS_AUX_IME, + DUMMY_SETTING_ACTIVITY_NAME, subtypes, DUMMY_IS_DEFAULT_RES_ID, + DUMMY_FORCE_DEFAULT, supportsSwitchingToNextInputMethod); + if (subtypes == null) { + items.add(new ImeSubtypeListItem(imeName, null /* variableName */, imi, + NOT_A_SUBTYPE_ID, null, SYSTEM_LOCALE)); + } else { + for (int i = 0; i < subtypes.size(); ++i) { + final String subtypeLocale = subtypeLocales.get(i); + items.add(new ImeSubtypeListItem(imeName, subtypeLocale, imi, i, subtypeLocale, + SYSTEM_LOCALE)); + } + } + } + + private static List createEnabledImeSubtypes() { + final List items = new ArrayList(); + addDummyImeSubtypeListItems(items, "LatinIme", "LatinIme", Arrays.asList("en_US", "fr"), + true /* supportsSwitchingToNextInputMethod*/); + addDummyImeSubtypeListItems(items, "switchUnawareLatinIme", "switchUnawareLatinIme", + Arrays.asList("en_UK", "hi"), + false /* supportsSwitchingToNextInputMethod*/); + addDummyImeSubtypeListItems(items, "subtypeUnawareIme", "subtypeUnawareIme", null, + false /* supportsSwitchingToNextInputMethod*/); + addDummyImeSubtypeListItems(items, "JapaneseIme", "JapaneseIme", Arrays.asList("ja_JP"), + true /* supportsSwitchingToNextInputMethod*/); + addDummyImeSubtypeListItems(items, "switchUnawareJapaneseIme", "switchUnawareJapaneseIme", + Arrays.asList("ja_JP"), false /* supportsSwitchingToNextInputMethod*/); + return items; + } + + private static List createDisabledImeSubtypes() { + final List items = new ArrayList(); + addDummyImeSubtypeListItems(items, + "UnknownIme", "UnknownIme", + Arrays.asList("en_US", "hi"), + true /* supportsSwitchingToNextInputMethod*/); + addDummyImeSubtypeListItems(items, + "UnknownSwitchingUnawareIme", "UnknownSwitchingUnawareIme", + Arrays.asList("en_US"), + false /* supportsSwitchingToNextInputMethod*/); + addDummyImeSubtypeListItems(items, "UnknownSubtypeUnawareIme", + "UnknownSubtypeUnawareIme", null, + false /* supportsSwitchingToNextInputMethod*/); + return items; + } + + private void assertNextInputMethod(final ControllerImpl controller, + final boolean onlyCurrentIme, + final ImeSubtypeListItem currentItem, final ImeSubtypeListItem nextItem) { + InputMethodSubtype subtype = null; + if (currentItem.mSubtypeName != null) { + subtype = createDummySubtype(currentItem.mSubtypeName.toString()); + } + final ImeSubtypeListItem nextIme = controller.getNextInputMethod(onlyCurrentIme, + currentItem.mImi, subtype); + assertEquals(nextItem, nextIme); + } + + private void assertRotationOrder(final ControllerImpl controller, + final boolean onlyCurrentIme, + final ImeSubtypeListItem... expectedRotationOrderOfImeSubtypeList) { + final int N = expectedRotationOrderOfImeSubtypeList.length; + for (int i = 0; i < N; i++) { + final int currentIndex = i; + final int nextIndex = (currentIndex + 1) % N; + final ImeSubtypeListItem currentItem = + expectedRotationOrderOfImeSubtypeList[currentIndex]; + final ImeSubtypeListItem nextItem = expectedRotationOrderOfImeSubtypeList[nextIndex]; + assertNextInputMethod(controller, onlyCurrentIme, currentItem, nextItem); + } + } + + private void onUserAction(final ControllerImpl controller, + final ImeSubtypeListItem subtypeListItem) { + InputMethodSubtype subtype = null; + if (subtypeListItem.mSubtypeName != null) { + subtype = createDummySubtype(subtypeListItem.mSubtypeName.toString()); + } + controller.onUserActionLocked(subtypeListItem.mImi, subtype); + } + + @SmallTest + public void testControllerImpl() throws Exception { + final List disabledItems = createDisabledImeSubtypes(); + final ImeSubtypeListItem disabledIme_en_US = disabledItems.get(0); + final ImeSubtypeListItem disabledIme_hi = disabledItems.get(1); + final ImeSubtypeListItem disabledSwitchingUnawareIme = disabledItems.get(2); + final ImeSubtypeListItem disabledSubtypeUnawareIme = disabledItems.get(3); + + final List enabledItems = createEnabledImeSubtypes(); + final ImeSubtypeListItem latinIme_en_US = enabledItems.get(0); + final ImeSubtypeListItem latinIme_fr = enabledItems.get(1); + final ImeSubtypeListItem switchingUnawarelatinIme_en_UK = enabledItems.get(2); + final ImeSubtypeListItem switchingUnawarelatinIme_hi = enabledItems.get(3); + final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4); + final ImeSubtypeListItem japaneseIme_ja_JP = enabledItems.get(5); + final ImeSubtypeListItem switchUnawareJapaneseIme_ja_JP = enabledItems.get(6); + + final ControllerImpl controller = ControllerImpl.createFrom( + null /* currentInstance */, enabledItems); + + // switching-aware loop + assertRotationOrder(controller, false /* onlyCurrentIme */, + latinIme_en_US, latinIme_fr, japaneseIme_ja_JP); + + // switching-unaware loop + assertRotationOrder(controller, false /* onlyCurrentIme */, + switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme, + switchUnawareJapaneseIme_ja_JP); + + // test onlyCurrentIme == true + assertRotationOrder(controller, true /* onlyCurrentIme */, + latinIme_en_US, latinIme_fr); + assertRotationOrder(controller, true /* onlyCurrentIme */, + switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi); + assertNextInputMethod(controller, true /* onlyCurrentIme */, + subtypeUnawareIme, null); + assertNextInputMethod(controller, true /* onlyCurrentIme */, + japaneseIme_ja_JP, null); + assertNextInputMethod(controller, true /* onlyCurrentIme */, + switchUnawareJapaneseIme_ja_JP, null); + + // Make sure that disabled IMEs are not accepted. + assertNextInputMethod(controller, false /* onlyCurrentIme */, + disabledIme_en_US, null); + assertNextInputMethod(controller, false /* onlyCurrentIme */, + disabledIme_hi, null); + assertNextInputMethod(controller, false /* onlyCurrentIme */, + disabledSwitchingUnawareIme, null); + assertNextInputMethod(controller, false /* onlyCurrentIme */, + disabledSubtypeUnawareIme, null); + assertNextInputMethod(controller, true /* onlyCurrentIme */, + disabledIme_en_US, null); + assertNextInputMethod(controller, true /* onlyCurrentIme */, + disabledIme_hi, null); + assertNextInputMethod(controller, true /* onlyCurrentIme */, + disabledSwitchingUnawareIme, null); + assertNextInputMethod(controller, true /* onlyCurrentIme */, + disabledSubtypeUnawareIme, null); + } + + @SmallTest + public void testControllerImplWithUserAction() throws Exception { + final List enabledItems = createEnabledImeSubtypes(); + final ImeSubtypeListItem latinIme_en_US = enabledItems.get(0); + final ImeSubtypeListItem latinIme_fr = enabledItems.get(1); + final ImeSubtypeListItem switchingUnawarelatinIme_en_UK = enabledItems.get(2); + final ImeSubtypeListItem switchingUnawarelatinIme_hi = enabledItems.get(3); + final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4); + final ImeSubtypeListItem japaneseIme_ja_JP = enabledItems.get(5); + final ImeSubtypeListItem switchUnawareJapaneseIme_ja_JP = enabledItems.get(6); + + final ControllerImpl controller = ControllerImpl.createFrom( + null /* currentInstance */, enabledItems); + + // === switching-aware loop === + assertRotationOrder(controller, false /* onlyCurrentIme */, + latinIme_en_US, latinIme_fr, japaneseIme_ja_JP); + // Then notify that a user did something for latinIme_fr. + onUserAction(controller, latinIme_fr); + assertRotationOrder(controller, false /* onlyCurrentIme */, + latinIme_fr, latinIme_en_US, japaneseIme_ja_JP); + // Then notify that a user did something for latinIme_fr again. + onUserAction(controller, latinIme_fr); + assertRotationOrder(controller, false /* onlyCurrentIme */, + latinIme_fr, latinIme_en_US, japaneseIme_ja_JP); + // Then notify that a user did something for japaneseIme_ja_JP. + onUserAction(controller, latinIme_fr); + assertRotationOrder(controller, false /* onlyCurrentIme */, + japaneseIme_ja_JP, latinIme_fr, latinIme_en_US); + // Check onlyCurrentIme == true. + assertNextInputMethod(controller, true /* onlyCurrentIme */, + japaneseIme_ja_JP, null); + assertRotationOrder(controller, true /* onlyCurrentIme */, + latinIme_fr, latinIme_en_US); + assertRotationOrder(controller, true /* onlyCurrentIme */, + latinIme_en_US, latinIme_fr); + + // === switching-unaware loop === + assertRotationOrder(controller, false /* onlyCurrentIme */, + switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme, + switchUnawareJapaneseIme_ja_JP); + // User action should be ignored for switching unaware IMEs. + onUserAction(controller, switchingUnawarelatinIme_hi); + assertRotationOrder(controller, false /* onlyCurrentIme */, + switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme, + switchUnawareJapaneseIme_ja_JP); + // User action should be ignored for switching unaware IMEs. + onUserAction(controller, switchUnawareJapaneseIme_ja_JP); + assertRotationOrder(controller, false /* onlyCurrentIme */, + switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme, + switchUnawareJapaneseIme_ja_JP); + // Check onlyCurrentIme == true. + assertRotationOrder(controller, true /* onlyCurrentIme */, + switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi); + assertNextInputMethod(controller, true /* onlyCurrentIme */, + subtypeUnawareIme, null); + assertNextInputMethod(controller, true /* onlyCurrentIme */, + switchUnawareJapaneseIme_ja_JP, null); + + // Rotation order should be preserved when created with the same subtype list. + final List sameEnabledItems = createEnabledImeSubtypes(); + final ControllerImpl newController = ControllerImpl.createFrom(controller, + sameEnabledItems); + assertRotationOrder(newController, false /* onlyCurrentIme */, + japaneseIme_ja_JP, latinIme_fr, latinIme_en_US); + assertRotationOrder(newController, false /* onlyCurrentIme */, + switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme, + switchUnawareJapaneseIme_ja_JP); + + // Rotation order should be initialized when created with a different subtype list. + final List differentEnabledItems = Arrays.asList( + latinIme_en_US, latinIme_fr, switchingUnawarelatinIme_en_UK, + switchUnawareJapaneseIme_ja_JP); + final ControllerImpl anotherController = ControllerImpl.createFrom(controller, + differentEnabledItems); + assertRotationOrder(anotherController, false /* onlyCurrentIme */, + latinIme_en_US, latinIme_fr); + assertRotationOrder(anotherController, false /* onlyCurrentIme */, + switchingUnawarelatinIme_en_UK, switchUnawareJapaneseIme_ja_JP); + } + + @SmallTest + public void testImeSubtypeListItem() throws Exception { + final List items = new ArrayList(); + addDummyImeSubtypeListItems(items, "LatinIme", "LatinIme", + Arrays.asList("en_US", "fr", "en", "en_uk", "enn", "e", "EN_US"), + true /* supportsSwitchingToNextInputMethod*/); + final ImeSubtypeListItem item_en_US = items.get(0); + final ImeSubtypeListItem item_fr = items.get(1); + final ImeSubtypeListItem item_en = items.get(2); + final ImeSubtypeListItem item_enn = items.get(3); + final ImeSubtypeListItem item_e = items.get(4); + final ImeSubtypeListItem item_EN_US = items.get(5); + + assertTrue(item_en_US.mIsSystemLocale); + assertFalse(item_fr.mIsSystemLocale); + assertFalse(item_en.mIsSystemLocale); + assertFalse(item_en.mIsSystemLocale); + assertFalse(item_enn.mIsSystemLocale); + assertFalse(item_e.mIsSystemLocale); + assertFalse(item_EN_US.mIsSystemLocale); + + assertTrue(item_en_US.mIsSystemLanguage); + assertFalse(item_fr.mIsSystemLanguage); + assertTrue(item_en.mIsSystemLanguage); + assertFalse(item_enn.mIsSystemLocale); + assertFalse(item_e.mIsSystemLocale); + assertFalse(item_EN_US.mIsSystemLocale); + } +} diff --git a/src/main/java/android/os/InputMethodSubtypeTest.java b/src/main/java/android/os/InputMethodSubtypeTest.java new file mode 100644 index 0000000..323a360 --- /dev/null +++ b/src/main/java/android/os/InputMethodSubtypeTest.java @@ -0,0 +1,101 @@ +/* + * 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 android.os; + +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.inputmethod.InputMethodSubtype; +import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; + +import java.util.Objects; + +public class InputMethodSubtypeTest extends InstrumentationTestCase { + + public void verifyLocale(final String localeString) { + // InputMethodSubtype#getLocale() returns exactly the same string that is passed to the + // constructor. + assertEquals(localeString, createDummySubtype(localeString).getLocale()); + + // InputMethodSubtype#getLocale() should be preserved via marshaling. + assertEquals(createDummySubtype(localeString).getLocale(), + cloneViaParcel(createDummySubtype(localeString)).getLocale()); + + // InputMethodSubtype#getLocale() should be preserved via marshaling. + assertEquals(createDummySubtype(localeString).getLocale(), + cloneViaParcel(cloneViaParcel(createDummySubtype(localeString))).getLocale()); + + // Make sure InputMethodSubtype#hashCode() returns the same hash code. + assertEquals(createDummySubtype(localeString).hashCode(), + createDummySubtype(localeString).hashCode()); + assertEquals(createDummySubtype(localeString).hashCode(), + cloneViaParcel(createDummySubtype(localeString)).hashCode()); + assertEquals(createDummySubtype(localeString).hashCode(), + cloneViaParcel(cloneViaParcel(createDummySubtype(localeString))).hashCode()); + } + + @SmallTest + public void testLocaleString() throws Exception { + // The locale string in InputMethodSubtype has accepted an arbitrary text actually, + // regardless of the validity of the text as a locale string. + verifyLocale("en_US"); + verifyLocale("apparently invalid locale string"); + verifyLocale("zz"); + verifyLocale("iw"); + verifyLocale("he"); + } + + @SmallTest + public void testDeprecatedLocaleString() throws Exception { + // Make sure "iw" is not automatically replaced with "he". + final InputMethodSubtype subtypeIw = createDummySubtype("iw"); + final InputMethodSubtype subtypeHe = createDummySubtype("he"); + assertEquals("iw", subtypeIw.getLocale()); + assertEquals("he", subtypeHe.getLocale()); + assertFalse(Objects.equals(subtypeIw, subtypeHe)); + assertFalse(Objects.equals(subtypeIw.hashCode(), subtypeHe.hashCode())); + + final InputMethodSubtype clonedSubtypeIw = cloneViaParcel(subtypeIw); + final InputMethodSubtype clonedSubtypeHe = cloneViaParcel(subtypeHe); + assertEquals(subtypeIw, clonedSubtypeIw); + assertEquals(subtypeHe, clonedSubtypeHe); + assertEquals("iw", clonedSubtypeIw.getLocale()); + assertEquals("he", clonedSubtypeHe.getLocale()); + } + + private static final InputMethodSubtype cloneViaParcel(final InputMethodSubtype original) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + original.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return InputMethodSubtype.CREATOR.createFromParcel(parcel); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } + + private static final InputMethodSubtype createDummySubtype(final String locale) { + final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder(); + return builder.setSubtypeNameResId(0) + .setSubtypeIconResId(0) + .setSubtypeLocale(locale) + .setIsAsciiCapable(true) + .build(); + } +} diff --git a/src/main/java/android/os/InputMethodTest.java b/src/main/java/android/os/InputMethodTest.java new file mode 100644 index 0000000..1557918 --- /dev/null +++ b/src/main/java/android/os/InputMethodTest.java @@ -0,0 +1,398 @@ +/* + * 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 android.os; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; +import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; + +import com.android.internal.inputmethod.InputMethodUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +public class InputMethodTest extends InstrumentationTestCase { + private static final boolean IS_AUX = true; + private static final boolean IS_DEFAULT = true; + private static final boolean IS_AUTO = true; + private static final boolean IS_ASCII_CAPABLE = true; + private static final boolean IS_SYSTEM_READY = true; + private static final ArrayList NO_SUBTYPE = null; + private static final Locale LOCALE_EN_US = new Locale("en", "US"); + private static final Locale LOCALE_EN_GB = new Locale("en", "GB"); + private static final Locale LOCALE_EN_IN = new Locale("en", "IN"); + private static final Locale LOCALE_HI = new Locale("hi"); + private static final Locale LOCALE_JA_JP = new Locale("ja", "JP"); + private static final Locale LOCALE_ZH_CN = new Locale("zh", "CN"); + private static final Locale LOCALE_ZH_TW = new Locale("zh", "TW"); + private static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; + private static final String SUBTYPE_MODE_VOICE = "voice"; + + @SmallTest + public void testVoiceImes() throws Exception { + // locale: en_US + assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_EN_US, + !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); + assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_EN_US, + !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme"); + assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_EN_US, + IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); + assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_EN_US, + IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyNonDefaultAutoVoiceIme0", + "DummyNonDefaultAutoVoiceIme1"); + + // locale: en_GB + assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_EN_GB, + !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); + assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_EN_GB, + !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme"); + assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_EN_GB, + IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); + assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_EN_GB, + IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyNonDefaultAutoVoiceIme0", + "DummyNonDefaultAutoVoiceIme1"); + + // locale: ja_JP + assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_JA_JP, + !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); + assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_JA_JP, + !IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme"); + assertDefaultEnabledImes(getImesWithDefaultVoiceIme(), LOCALE_JA_JP, + IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyDefaultAutoVoiceIme"); + assertDefaultEnabledImes(getImesWithoutDefaultVoiceIme(), LOCALE_JA_JP, + IS_SYSTEM_READY, "DummyDefaultEnKeyboardIme", "DummyNonDefaultAutoVoiceIme0", + "DummyNonDefaultAutoVoiceIme1"); + } + + @SmallTest + public void testKeyboardImes() throws Exception { + // locale: en_US + assertDefaultEnabledImes(getSamplePreinstalledImes("en-rUS"), LOCALE_EN_US, + !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); + assertDefaultEnabledImes(getSamplePreinstalledImes("en-rUS"), LOCALE_EN_US, + IS_SYSTEM_READY, "com.android.apps.inputmethod.latin", + "com.android.apps.inputmethod.voice"); + + // locale: en_GB + assertDefaultEnabledImes(getSamplePreinstalledImes("en-rGB"), LOCALE_EN_GB, + !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); + assertDefaultEnabledImes(getSamplePreinstalledImes("en-rGB"), LOCALE_EN_GB, + IS_SYSTEM_READY, "com.android.apps.inputmethod.latin", + "com.android.apps.inputmethod.voice"); + + // locale: en_IN + assertDefaultEnabledImes(getSamplePreinstalledImes("en-rIN"), LOCALE_EN_IN, + !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); + assertDefaultEnabledImes(getSamplePreinstalledImes("en-rIN"), LOCALE_EN_IN, + IS_SYSTEM_READY, "com.android.apps.inputmethod.hindi", + "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.voice"); + + // locale: hi + assertDefaultEnabledImes(getSamplePreinstalledImes("hi"), LOCALE_HI, + !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); + assertDefaultEnabledImes(getSamplePreinstalledImes("hi"), LOCALE_HI, + IS_SYSTEM_READY, "com.android.apps.inputmethod.hindi", + "com.android.apps.inputmethod.latin", "com.android.apps.inputmethod.voice"); + + // locale: ja_JP + assertDefaultEnabledImes(getSamplePreinstalledImes("ja-rJP"), LOCALE_JA_JP, + !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); + assertDefaultEnabledImes(getSamplePreinstalledImes("ja-rJP"), LOCALE_JA_JP, + IS_SYSTEM_READY, "com.android.apps.inputmethod.japanese", + "com.android.apps.inputmethod.voice"); + + // locale: zh_CN + assertDefaultEnabledImes(getSamplePreinstalledImes("zh-rCN"), LOCALE_ZH_CN, + !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); + assertDefaultEnabledImes(getSamplePreinstalledImes("zh-rCN"), LOCALE_ZH_CN, + IS_SYSTEM_READY, "com.android.apps.inputmethod.pinyin", + "com.android.apps.inputmethod.voice"); + + // locale: zh_TW + // Note: In this case, no IME is suitable for the system locale. Hence we will pick up a + // fallback IME regardless of the "default" attribute. + assertDefaultEnabledImes(getSamplePreinstalledImes("zh-rTW"), LOCALE_ZH_TW, + !IS_SYSTEM_READY, "com.android.apps.inputmethod.latin"); + assertDefaultEnabledImes(getSamplePreinstalledImes("zh-rTW"), LOCALE_ZH_TW, + IS_SYSTEM_READY, "com.android.apps.inputmethod.latin", + "com.android.apps.inputmethod.voice"); + } + + @SmallTest + public void testParcelable() throws Exception { + final ArrayList originalList = getSamplePreinstalledImes("en-rUS"); + final List clonedList = cloneViaParcel(originalList); + assertNotNull(clonedList); + final List clonedClonedList = cloneViaParcel(clonedList); + assertNotNull(clonedClonedList); + assertEquals(originalList, clonedList); + assertEquals(clonedList, clonedClonedList); + assertEquals(originalList.size(), clonedList.size()); + assertEquals(clonedList.size(), clonedClonedList.size()); + for (int imeIndex = 0; imeIndex < originalList.size(); ++imeIndex) { + verifyEquality(originalList.get(imeIndex), clonedList.get(imeIndex)); + verifyEquality(clonedList.get(imeIndex), clonedClonedList.get(imeIndex)); + } + } + + private void assertDefaultEnabledImes(final ArrayList preinstalledImes, + final Locale systemLocale, final boolean isSystemReady, String... expectedImeNames) { + final Context context = getInstrumentation().getTargetContext(); + final String[] actualImeNames = getPackageNames(callGetDefaultEnabledImesUnderWithLocale( + context, isSystemReady, preinstalledImes, systemLocale)); + assertEquals(expectedImeNames.length, actualImeNames.length); + for (int i = 0; i < expectedImeNames.length; ++i) { + assertEquals(expectedImeNames[i], actualImeNames[i]); + } + } + + private static List cloneViaParcel(final List list) { + Parcel p = null; + try { + p = Parcel.obtain(); + p.writeTypedList(list); + p.setDataPosition(0); + return p.createTypedArrayList(InputMethodInfo.CREATOR); + } finally { + if (p != null) { + p.recycle(); + } + } + } + + private static ArrayList callGetDefaultEnabledImesUnderWithLocale( + final Context context, final boolean isSystemReady, + final ArrayList imis, final Locale locale) { + final Locale initialLocale = context.getResources().getConfiguration().locale; + try { + context.getResources().getConfiguration().setLocale(locale); + return InputMethodUtils.getDefaultEnabledImes(context, isSystemReady, imis); + } finally { + context.getResources().getConfiguration().setLocale(initialLocale); + } + } + + private String[] getPackageNames(final ArrayList imis) { + final String[] packageNames = new String[imis.size()]; + for (int i = 0; i < imis.size(); ++i) { + packageNames[i] = imis.get(i).getPackageName(); + } + return packageNames; + } + + private static void verifyEquality(InputMethodInfo expected, InputMethodInfo actual) { + assertEquals(expected, actual); + assertEquals(expected.getSubtypeCount(), actual.getSubtypeCount()); + for (int subtypeIndex = 0; subtypeIndex < expected.getSubtypeCount(); ++subtypeIndex) { + final InputMethodSubtype expectedSubtype = expected.getSubtypeAt(subtypeIndex); + final InputMethodSubtype actualSubtype = actual.getSubtypeAt(subtypeIndex); + assertEquals(expectedSubtype, actualSubtype); + assertEquals(expectedSubtype.hashCode(), actualSubtype.hashCode()); + } + } + + private static InputMethodInfo createDummyInputMethodInfo(String packageName, String name, + CharSequence label, boolean isAuxIme, boolean isDefault, + List subtypes) { + final ResolveInfo ri = new ResolveInfo(); + final ServiceInfo si = new ServiceInfo(); + final ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + ai.enabled = true; + ai.flags |= ApplicationInfo.FLAG_SYSTEM; + si.applicationInfo = ai; + si.enabled = true; + si.packageName = packageName; + si.name = name; + si.exported = true; + si.nonLocalizedLabel = label; + ri.serviceInfo = si; + return new InputMethodInfo(ri, isAuxIme, "", subtypes, 1, isDefault); + } + + private static InputMethodSubtype createDummyInputMethodSubtype(String locale, String mode, + boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype, + boolean isAsciiCapable) { + return new InputMethodSubtypeBuilder() + .setSubtypeNameResId(0) + .setSubtypeIconResId(0) + .setSubtypeLocale(locale) + .setSubtypeMode(mode) + .setSubtypeExtraValue("") + .setIsAuxiliary(isAuxiliary) + .setOverridesImplicitlyEnabledSubtype(overridesImplicitlyEnabledSubtype) + .setIsAsciiCapable(isAsciiCapable) + .build(); + } + + private static ArrayList getImesWithDefaultVoiceIme() { + ArrayList preinstalledImes = new ArrayList<>(); + { + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummyInputMethodSubtype("auto", SUBTYPE_MODE_VOICE, IS_AUX, IS_AUTO, + !IS_ASCII_CAPABLE)); + subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_VOICE, IS_AUX, + !IS_AUTO, !IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("DummyDefaultAutoVoiceIme", + "dummy.voice0", "DummyVoice0", IS_AUX, IS_DEFAULT, subtypes)); + } + preinstalledImes.addAll(getImesWithoutDefaultVoiceIme()); + return preinstalledImes; + } + + private static ArrayList getImesWithoutDefaultVoiceIme() { + ArrayList preinstalledImes = new ArrayList<>(); + { + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummyInputMethodSubtype("auto", SUBTYPE_MODE_VOICE, IS_AUX, IS_AUTO, + !IS_ASCII_CAPABLE)); + subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_VOICE, IS_AUX, + !IS_AUTO, !IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("DummyNonDefaultAutoVoiceIme0", + "dummy.voice1", "DummyVoice1", IS_AUX, !IS_DEFAULT, subtypes)); + } + { + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummyInputMethodSubtype("auto", SUBTYPE_MODE_VOICE, IS_AUX, IS_AUTO, + !IS_ASCII_CAPABLE)); + subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_VOICE, IS_AUX, + !IS_AUTO, !IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("DummyNonDefaultAutoVoiceIme1", + "dummy.voice2", "DummyVoice2", IS_AUX, !IS_DEFAULT, subtypes)); + } + { + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_VOICE, IS_AUX, + !IS_AUTO, !IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("DummyNonDefaultVoiceIme2", + "dummy.voice3", "DummyVoice3", IS_AUX, !IS_DEFAULT, subtypes)); + } + { + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("DummyDefaultEnKeyboardIme", + "dummy.keyboard0", "DummyKeyboard0", !IS_AUX, IS_DEFAULT, subtypes)); + } + return preinstalledImes; + } + + private static boolean contains(final String[] textList, final String textToBeChecked) { + if (textList == null) { + return false; + } + for (final String text : textList) { + if (Objects.equals(textToBeChecked, text)) { + return true; + } + } + return false; + } + + private static ArrayList getSamplePreinstalledImes(final String localeString) { + ArrayList preinstalledImes = new ArrayList<>(); + + // a dummy Voice IME + { + final boolean isDefaultIme = false; + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummyInputMethodSubtype("", SUBTYPE_MODE_VOICE, IS_AUX, + IS_AUTO, !IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.voice", + "com.android.inputmethod.voice", "DummyVoiceIme", IS_AUX, isDefaultIme, + subtypes)); + } + // a dummy Hindi IME + { + final boolean isDefaultIme = contains(new String[]{ "hi", "en-rIN" }, localeString); + final ArrayList subtypes = new ArrayList(); + // TODO: This subtype should be marked as IS_ASCII_CAPABLE + subtypes.add(createDummyInputMethodSubtype("en_IN", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, !IS_ASCII_CAPABLE)); + subtypes.add(createDummyInputMethodSubtype("hi", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, !IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.hindi", + "com.android.inputmethod.hindi", "DummyHindiIme", !IS_AUX, isDefaultIme, + subtypes)); + } + + // a dummy Pinyin IME + { + final boolean isDefaultIme = contains(new String[]{ "zh-rCN" }, localeString); + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummyInputMethodSubtype("zh_CN", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, !IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.pinyin", + "com.android.apps.inputmethod.pinyin", "DummyPinyinIme", !IS_AUX, isDefaultIme, + subtypes)); + } + + // a dummy Korean IME + { + final boolean isDefaultIme = contains(new String[]{ "ko" }, localeString); + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummyInputMethodSubtype("ko", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, !IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.korean", + "com.android.apps.inputmethod.korean", "DummyKoreanIme", !IS_AUX, isDefaultIme, + subtypes)); + } + + // a dummy Latin IME + { + final boolean isDefaultIme = contains( + new String[]{ "en-rUS", "en-rGB", "en-rIN", "en", "hi" }, localeString); + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummyInputMethodSubtype("en_US", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, IS_ASCII_CAPABLE)); + subtypes.add(createDummyInputMethodSubtype("en_GB", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, IS_ASCII_CAPABLE)); + subtypes.add(createDummyInputMethodSubtype("en_IN", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, IS_ASCII_CAPABLE)); + subtypes.add(createDummyInputMethodSubtype("hi", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.latin", + "com.android.apps.inputmethod.latin", "DummyLatinIme", !IS_AUX, isDefaultIme, + subtypes)); + } + + // a dummy Japanese IME + { + final boolean isDefaultIme = contains(new String[]{ "ja", "ja-rJP" }, localeString); + final ArrayList subtypes = new ArrayList(); + subtypes.add(createDummyInputMethodSubtype("ja", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, !IS_ASCII_CAPABLE)); + subtypes.add(createDummyInputMethodSubtype("emoji", SUBTYPE_MODE_KEYBOARD, !IS_AUX, + !IS_AUTO, !IS_ASCII_CAPABLE)); + preinstalledImes.add(createDummyInputMethodInfo("com.android.apps.inputmethod.japanese", + "com.android.apps.inputmethod.japanese", "DummyJapaneseIme", !IS_AUX, + isDefaultIme, subtypes)); + } + + return preinstalledImes; + } +} diff --git a/src/main/java/android/os/Looper.java b/src/main/java/android/os/Looper.java new file mode 100644 index 0000000..6d7c9cf --- /dev/null +++ b/src/main/java/android/os/Looper.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.util.Log; +import android.util.Printer; + +/** + * Class used to run a message loop for a thread. Threads by default do + * not have a message loop associated with them; to create one, call + * {@link #prepare} in the thread that is to run the loop, and then + * {@link #loop} to have it process messages until the loop is stopped. + * + *

Most interaction with a message loop is through the + * {@link Handler} class. + * + *

This is a typical example of the implementation of a Looper thread, + * using the separation of {@link #prepare} and {@link #loop} to create an + * initial Handler to communicate with the Looper. + * + *

+  *  class LooperThread extends Thread {
+  *      public Handler mHandler;
+  *
+  *      public void run() {
+  *          Looper.prepare();
+  *
+  *          mHandler = new Handler() {
+  *              public void handleMessage(Message msg) {
+  *                  // process incoming messages here
+  *              }
+  *          };
+  *
+  *          Looper.loop();
+  *      }
+  *  }
+ */ +public final class Looper { + private static final String TAG = "Looper"; + + // sThreadLocal.get() will return null unless you've called prepare(). + static final ThreadLocal sThreadLocal = new ThreadLocal(); + private static Looper sMainLooper; // guarded by Looper.class + + final MessageQueue mQueue; + final Thread mThread; + + private Printer mLogging; + + /** Initialize the current thread as a looper. + * This gives you a chance to create handlers that then reference + * this looper, before actually starting the loop. Be sure to call + * {@link #loop()} after calling this method, and end it by calling + * {@link #quit()}. + */ + public static void prepare() { + prepare(true); + } + + private static void prepare(boolean quitAllowed) { + if (sThreadLocal.get() != null) { + throw new RuntimeException("Only one Looper may be created per thread"); + } + sThreadLocal.set(new Looper(quitAllowed)); + } + + /** + * Initialize the current thread as a looper, marking it as an + * application's main looper. The main looper for your application + * is created by the Android environment, so you should never need + * to call this function yourself. See also: {@link #prepare()} + */ + public static void prepareMainLooper() { + prepare(false); + synchronized (Looper.class) { + if (sMainLooper != null) { + throw new IllegalStateException("The main Looper has already been prepared."); + } + sMainLooper = myLooper(); + } + } + + /** Returns the application's main looper, which lives in the main thread of the application. + */ + public static Looper getMainLooper() { + synchronized (Looper.class) { + return sMainLooper; + } + } + + /** + * Run the message queue in this thread. Be sure to call + * {@link #quit()} to end the loop. + */ + public static void loop() { + final Looper me = myLooper(); + if (me == null) { + throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); + } + final MessageQueue queue = me.mQueue; + + // Make sure the identity of this thread is that of the local process, + // and keep track of what that identity token actually is. + Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); + + for (;;) { + Message msg = queue.next(); // might block + if (msg == null) { + // No message indicates that the message queue is quitting. + return; + } + + // This must be in a local variable, in case a UI event sets the logger + Printer logging = me.mLogging; + if (logging != null) { + logging.println(">>>>> Dispatching to " + msg.target + " " + + msg.callback + ": " + msg.what); + } + + msg.target.dispatchMessage(msg); + + if (logging != null) { + logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); + } + + // Make sure that during the course of dispatching the + // identity of the thread wasn't corrupted. + final long newIdent = Binder.clearCallingIdentity(); + if (ident != newIdent) { + Log.wtf(TAG, "Thread identity changed from 0x" + + Long.toHexString(ident) + " to 0x" + + Long.toHexString(newIdent) + " while dispatching to " + + msg.target.getClass().getName() + " " + + msg.callback + " what=" + msg.what); + } + + msg.recycleUnchecked(); + } + } + + /** + * Return the Looper object associated with the current thread. Returns + * null if the calling thread is not associated with a Looper. + */ + public static Looper myLooper() { + return sThreadLocal.get(); + } + + /** + * Control logging of messages as they are processed by this Looper. If + * enabled, a log message will be written to printer + * at the beginning and ending of each message dispatch, identifying the + * target Handler and message contents. + * + * @param printer A Printer object that will receive log messages, or + * null to disable message logging. + */ + public void setMessageLogging(Printer printer) { + mLogging = printer; + } + + /** + * Return the {@link MessageQueue} object associated with the current + * thread. This must be called from a thread running a Looper, or a + * NullPointerException will be thrown. + */ + public static MessageQueue myQueue() { + return myLooper().mQueue; + } + + private Looper(boolean quitAllowed) { + mQueue = new MessageQueue(quitAllowed); + mThread = Thread.currentThread(); + } + + /** + * Returns true if the current thread is this looper's thread. + * @hide + */ + public boolean isCurrentThread() { + return Thread.currentThread() == mThread; + } + + /** + * Quits the looper. + *

+ * Causes the {@link #loop} method to terminate without processing any + * more messages in the message queue. + *

+ * Any attempt to post messages to the queue after the looper is asked to quit will fail. + * For example, the {@link Handler#sendMessage(Message)} method will return false. + *

+ * Using this method may be unsafe because some messages may not be delivered + * before the looper terminates. Consider using {@link #quitSafely} instead to ensure + * that all pending work is completed in an orderly manner. + *

+ * + * @see #quitSafely + */ + public void quit() { + mQueue.quit(false); + } + + /** + * Quits the looper safely. + *

+ * Causes the {@link #loop} method to terminate as soon as all remaining messages + * in the message queue that are already due to be delivered have been handled. + * However pending delayed messages with due times in the future will not be + * delivered before the loop terminates. + *

+ * Any attempt to post messages to the queue after the looper is asked to quit will fail. + * For example, the {@link Handler#sendMessage(Message)} method will return false. + *

+ */ + public void quitSafely() { + mQueue.quit(true); + } + + /** + * Posts a synchronization barrier to the Looper's message queue. + * + * Message processing occurs as usual until the message queue encounters the + * synchronization barrier that has been posted. When the barrier is encountered, + * later synchronous messages in the queue are stalled (prevented from being executed) + * until the barrier is released by calling {@link #removeSyncBarrier} and specifying + * the token that identifies the synchronization barrier. + * + * This method is used to immediately postpone execution of all subsequently posted + * synchronous messages until a condition is met that releases the barrier. + * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier + * and continue to be processed as usual. + * + * This call must be always matched by a call to {@link #removeSyncBarrier} with + * the same token to ensure that the message queue resumes normal operation. + * Otherwise the application will probably hang! + * + * @return A token that uniquely identifies the barrier. This token must be + * passed to {@link #removeSyncBarrier} to release the barrier. + * + * @hide + */ + public int postSyncBarrier() { + return mQueue.enqueueSyncBarrier(SystemClock.uptimeMillis()); + } + + + /** + * Removes a synchronization barrier. + * + * @param token The synchronization barrier token that was returned by + * {@link #postSyncBarrier}. + * + * @throws IllegalStateException if the barrier was not found. + * + * @hide + */ + public void removeSyncBarrier(int token) { + mQueue.removeSyncBarrier(token); + } + + /** + * Return the Thread associated with this Looper. + */ + public Thread getThread() { + return mThread; + } + + /** @hide */ + public MessageQueue getQueue() { + return mQueue; + } + + /** + * Return whether this looper's thread is currently idle, waiting for new work + * to do. This is intrinsically racy, since its state can change before you get + * the result back. + * @hide + */ + public boolean isIdling() { + return mQueue.isIdling(); + } + + public void dump(Printer pw, String prefix) { + pw.println(prefix + toString()); + mQueue.dump(pw, prefix + " "); + } + + public String toString() { + return "Looper (" + mThread.getName() + ", tid " + mThread.getId() + + ") {" + Integer.toHexString(System.identityHashCode(this)) + "}"; + } +} diff --git a/src/main/java/android/os/Looper_Accessor.java b/src/main/java/android/os/Looper_Accessor.java new file mode 100644 index 0000000..09f3e47 --- /dev/null +++ b/src/main/java/android/os/Looper_Accessor.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 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 android.os; + +import java.lang.reflect.Field; + +/** + * Class allowing access to package-protected methods/fields. + */ +public class Looper_Accessor { + + public static void cleanupThread() { + // clean up the looper + Looper.sThreadLocal.remove(); + try { + Field sMainLooper = Looper.class.getDeclaredField("sMainLooper"); + sMainLooper.setAccessible(true); + sMainLooper.set(null, null); + } catch (SecurityException e) { + catchReflectionException(); + } catch (IllegalArgumentException e) { + catchReflectionException(); + } catch (NoSuchFieldException e) { + catchReflectionException(); + } catch (IllegalAccessException e) { + catchReflectionException(); + } + + } + + private static void catchReflectionException() { + assert(false); + } +} diff --git a/src/main/java/android/os/MemoryFile.java b/src/main/java/android/os/MemoryFile.java new file mode 100644 index 0000000..6cec55a --- /dev/null +++ b/src/main/java/android/os/MemoryFile.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2008 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 android.os; + +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + + +/** + * MemoryFile is a wrapper for the Linux ashmem driver. + * MemoryFiles are backed by shared memory, which can be optionally + * set to be purgeable. + * Purgeable files may have their contents reclaimed by the kernel + * in low memory conditions (only if allowPurging is set to true). + * After a file is purged, attempts to read or write the file will + * cause an IOException to be thrown. + */ +public class MemoryFile +{ + private static String TAG = "MemoryFile"; + + // mmap(2) protection flags from + private static final int PROT_READ = 0x1; + private static final int PROT_WRITE = 0x2; + + private static native FileDescriptor native_open(String name, int length) throws IOException; + // returns memory address for ashmem region + private static native long native_mmap(FileDescriptor fd, int length, int mode) + throws IOException; + private static native void native_munmap(long addr, int length) throws IOException; + private static native void native_close(FileDescriptor fd); + private static native int native_read(FileDescriptor fd, long address, byte[] buffer, + int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException; + private static native void native_write(FileDescriptor fd, long address, byte[] buffer, + int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException; + private static native void native_pin(FileDescriptor fd, boolean pin) throws IOException; + private static native int native_get_size(FileDescriptor fd) throws IOException; + + private FileDescriptor mFD; // ashmem file descriptor + private long mAddress; // address of ashmem memory + private int mLength; // total length of our ashmem region + private boolean mAllowPurging = false; // true if our ashmem region is unpinned + + /** + * Allocates a new ashmem region. The region is initially not purgable. + * + * @param name optional name for the file (can be null). + * @param length of the memory file in bytes, must be non-negative. + * @throws IOException if the memory file could not be created. + */ + public MemoryFile(String name, int length) throws IOException { + mLength = length; + if (length >= 0) { + mFD = native_open(name, length); + } else { + throw new IOException("Invalid length: " + length); + } + + if (length > 0) { + mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE); + } else { + mAddress = 0; + } + } + + /** + * Closes the memory file. If there are no other open references to the memory + * file, it will be deleted. + */ + public void close() { + deactivate(); + if (!isClosed()) { + native_close(mFD); + } + } + + /** + * Unmaps the memory file from the process's memory space, but does not close it. + * After this method has been called, read and write operations through this object + * will fail, but {@link #getFileDescriptor()} will still return a valid file descriptor. + * + * @hide + */ + void deactivate() { + if (!isDeactivated()) { + try { + native_munmap(mAddress, mLength); + mAddress = 0; + } catch (IOException ex) { + Log.e(TAG, ex.toString()); + } + } + } + + /** + * Checks whether the memory file has been deactivated. + */ + private boolean isDeactivated() { + return mAddress == 0; + } + + /** + * Checks whether the memory file has been closed. + */ + private boolean isClosed() { + return !mFD.valid(); + } + + @Override + protected void finalize() { + if (!isClosed()) { + Log.e(TAG, "MemoryFile.finalize() called while ashmem still open"); + close(); + } + } + + /** + * Returns the length of the memory file. + * + * @return file length. + */ + public int length() { + return mLength; + } + + /** + * Is memory file purging enabled? + * + * @return true if the file may be purged. + */ + public boolean isPurgingAllowed() { + return mAllowPurging; + } + + /** + * Enables or disables purging of the memory file. + * + * @param allowPurging true if the operating system can purge the contents + * of the file in low memory situations + * @return previous value of allowPurging + */ + synchronized public boolean allowPurging(boolean allowPurging) throws IOException { + boolean oldValue = mAllowPurging; + if (oldValue != allowPurging) { + native_pin(mFD, !allowPurging); + mAllowPurging = allowPurging; + } + return oldValue; + } + + /** + * Creates a new InputStream for reading from the memory file. + * + @return InputStream + */ + public InputStream getInputStream() { + return new MemoryInputStream(); + } + + /** + * Creates a new OutputStream for writing to the memory file. + * + @return OutputStream + */ + public OutputStream getOutputStream() { + return new MemoryOutputStream(); + } + + /** + * Reads bytes from the memory file. + * Will throw an IOException if the file has been purged. + * + * @param buffer byte array to read bytes into. + * @param srcOffset offset into the memory file to read from. + * @param destOffset offset into the byte array buffer to read into. + * @param count number of bytes to read. + * @return number of bytes read. + * @throws IOException if the memory file has been purged or deactivated. + */ + public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count) + throws IOException { + if (isDeactivated()) { + throw new IOException("Can't read from deactivated memory file."); + } + if (destOffset < 0 || destOffset > buffer.length || count < 0 + || count > buffer.length - destOffset + || srcOffset < 0 || srcOffset > mLength + || count > mLength - srcOffset) { + throw new IndexOutOfBoundsException(); + } + return native_read(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging); + } + + /** + * Write bytes to the memory file. + * Will throw an IOException if the file has been purged. + * + * @param buffer byte array to write bytes from. + * @param srcOffset offset into the byte array buffer to write from. + * @param destOffset offset into the memory file to write to. + * @param count number of bytes to write. + * @throws IOException if the memory file has been purged or deactivated. + */ + public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count) + throws IOException { + if (isDeactivated()) { + throw new IOException("Can't write to deactivated memory file."); + } + if (srcOffset < 0 || srcOffset > buffer.length || count < 0 + || count > buffer.length - srcOffset + || destOffset < 0 || destOffset > mLength + || count > mLength - destOffset) { + throw new IndexOutOfBoundsException(); + } + native_write(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging); + } + + /** + * Gets a FileDescriptor for the memory file. + * + * The returned file descriptor is not duplicated. + * + * @throws IOException If the memory file has been closed. + * + * @hide + */ + public FileDescriptor getFileDescriptor() throws IOException { + return mFD; + } + + /** + * Returns the size of the memory file that the file descriptor refers to, + * or -1 if the file descriptor does not refer to a memory file. + * + * @throws IOException If fd is not a valid file descriptor. + * + * @hide + */ + public static int getSize(FileDescriptor fd) throws IOException { + return native_get_size(fd); + } + + private class MemoryInputStream extends InputStream { + + private int mMark = 0; + private int mOffset = 0; + private byte[] mSingleByte; + + @Override + public int available() throws IOException { + if (mOffset >= mLength) { + return 0; + } + return mLength - mOffset; + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void mark(int readlimit) { + mMark = mOffset; + } + + @Override + public void reset() throws IOException { + mOffset = mMark; + } + + @Override + public int read() throws IOException { + if (mSingleByte == null) { + mSingleByte = new byte[1]; + } + int result = read(mSingleByte, 0, 1); + if (result != 1) { + return -1; + } + return mSingleByte[0]; + } + + @Override + public int read(byte buffer[], int offset, int count) throws IOException { + if (offset < 0 || count < 0 || offset + count > buffer.length) { + // readBytes() also does this check, but we need to do it before + // changing count. + throw new IndexOutOfBoundsException(); + } + count = Math.min(count, available()); + if (count < 1) { + return -1; + } + int result = readBytes(buffer, mOffset, offset, count); + if (result > 0) { + mOffset += result; + } + return result; + } + + @Override + public long skip(long n) throws IOException { + if (mOffset + n > mLength) { + n = mLength - mOffset; + } + mOffset += n; + return n; + } + } + + private class MemoryOutputStream extends OutputStream { + + private int mOffset = 0; + private byte[] mSingleByte; + + @Override + public void write(byte buffer[], int offset, int count) throws IOException { + writeBytes(buffer, offset, mOffset, count); + mOffset += count; + } + + @Override + public void write(int oneByte) throws IOException { + if (mSingleByte == null) { + mSingleByte = new byte[1]; + } + mSingleByte[0] = (byte)oneByte; + write(mSingleByte, 0, 1); + } + } +} diff --git a/src/main/java/android/os/MemoryFileTest.java b/src/main/java/android/os/MemoryFileTest.java new file mode 100644 index 0000000..82af662 --- /dev/null +++ b/src/main/java/android/os/MemoryFileTest.java @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2008 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 android.os; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.SmallTest; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class MemoryFileTest extends AndroidTestCase { + + private void compareBuffers(byte[] buffer1, byte[] buffer2, int length) throws Exception { + for (int i = 0; i < length; i++) { + if (buffer1[i] != buffer2[i]) { + throw new Exception("readBytes did not read back what writeBytes wrote"); + } + } + } + + /** + * Keep allocating new files till the system purges them. + */ + // Flaky test - temporarily suppress from large suite for now + // @LargeTest + public void testPurge() throws Exception { + List files = new ArrayList(); + try { + while (true) { + // This will fail if the process runs out of file descriptors before + // the kernel starts purging ashmem areas. + MemoryFile newFile = new MemoryFile("MemoryFileTest", 10000000); + newFile.allowPurging(true); + newFile.writeBytes(testString, 0, 0, testString.length); + files.add(newFile); + for (MemoryFile file : files) { + try { + file.readBytes(testString, 0, 0, testString.length); + } catch (IOException e) { + // Expected + return; + } + } + } + } finally { + for (MemoryFile fileToClose : files) { + fileToClose.close(); + } + } + } + + @SmallTest + public void testRun() throws Exception { + MemoryFile file = new MemoryFile("MemoryFileTest", 1000000); + + byte[] buffer = new byte[testString.length]; + + // check low level accessors + file.writeBytes(testString, 0, 2000, testString.length); + file.readBytes(buffer, 2000, 0, testString.length); + compareBuffers(testString, buffer, testString.length); + + // check streams + buffer = new byte[testString.length]; + + OutputStream os = file.getOutputStream(); + os.write(testString); + + InputStream is = file.getInputStream(); + is.mark(testString.length); + is.read(buffer); + compareBuffers(testString, buffer, testString.length); + + // test mark/reset + buffer = new byte[testString.length]; + is.reset(); + is.read(buffer); + compareBuffers(testString, buffer, testString.length); + + file.close(); + } + + // http://code.google.com/p/android/issues/detail?id=11415 + public void testOutputStreamAdvances() throws IOException { + MemoryFile file = new MemoryFile("MemoryFileTest", 10); + + OutputStream os = file.getOutputStream(); + os.write(new byte[] { 1, 2, 3, 4, 5 }); + os.write(new byte[] { -1, -1, 6, 7, 8, -1 }, 2, 3); + os.write(9); + try { + os.write(new byte[] { -1, -1 }); + fail(); + } catch (IndexOutOfBoundsException expected) { + } + + byte[] copy = new byte[file.length()]; + file.readBytes(copy, 0, 0, file.length()); + assertEquals("[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]", Arrays.toString(copy)); + } + + // Tests for the IndexOutOfBoundsException cases in read(). + + private void readIndexOutOfBoundsException(int offset, int count, String msg) + throws Exception { + MemoryFile file = new MemoryFile("MemoryFileTest", testString.length); + try { + file.writeBytes(testString, 0, 0, testString.length); + InputStream is = file.getInputStream(); + byte[] buffer = new byte[testString.length + 10]; + try { + is.read(buffer, offset, count); + fail(msg); + } catch (IndexOutOfBoundsException ex) { + // this is what should happen + } finally { + is.close(); + } + } finally { + file.close(); + } + } + + @SmallTest + public void testReadNegativeOffset() throws Exception { + readIndexOutOfBoundsException(-1, 5, + "read() with negative offset should throw IndexOutOfBoundsException"); + } + + @SmallTest + public void testReadNegativeCount() throws Exception { + readIndexOutOfBoundsException(5, -1, + "read() with negative length should throw IndexOutOfBoundsException"); + } + + @SmallTest + public void testReadOffsetOverflow() throws Exception { + readIndexOutOfBoundsException(testString.length + 10, 5, + "read() with offset outside buffer should throw IndexOutOfBoundsException"); + } + + @SmallTest + public void testReadOffsetCountOverflow() throws Exception { + readIndexOutOfBoundsException(testString.length, 11, + "read() with offset + count outside buffer should throw IndexOutOfBoundsException"); + } + + // Test behavior of read() at end of file + @SmallTest + public void testReadEOF() throws Exception { + MemoryFile file = new MemoryFile("MemoryFileTest", testString.length); + try { + file.writeBytes(testString, 0, 0, testString.length); + InputStream is = file.getInputStream(); + try { + byte[] buffer = new byte[testString.length + 10]; + // read() with count larger than data should succeed, and return # of bytes read + assertEquals(testString.length, is.read(buffer)); + compareBuffers(testString, buffer, testString.length); + // Read at EOF should return -1 + assertEquals(-1, is.read()); + } finally { + is.close(); + } + } finally { + file.close(); + } + } + + // Tests that close() is idempotent + @SmallTest + public void testCloseClose() throws Exception { + MemoryFile file = new MemoryFile("MemoryFileTest", 1000000); + byte[] data = new byte[512]; + file.writeBytes(data, 0, 0, 128); + file.close(); + file.close(); + } + + // Tests that we can't read from a closed memory file + @SmallTest + public void testCloseRead() throws Exception { + MemoryFile file = new MemoryFile("MemoryFileTest", 1000000); + file.close(); + + try { + byte[] data = new byte[512]; + assertEquals(128, file.readBytes(data, 0, 0, 128)); + fail("readBytes() after close() did not throw IOException."); + } catch (IOException e) { + // this is what should happen + } + } + + // Tests that we can't write to a closed memory file + @SmallTest + public void testCloseWrite() throws Exception { + MemoryFile file = new MemoryFile("MemoryFileTest", 1000000); + file.close(); + + try { + byte[] data = new byte[512]; + file.writeBytes(data, 0, 0, 128); + fail("writeBytes() after close() did not throw IOException."); + } catch (IOException e) { + // this is what should happen + } + } + + // Tests that we can't call allowPurging() after close() + @SmallTest + public void testCloseAllowPurging() throws Exception { + MemoryFile file = new MemoryFile("MemoryFileTest", 1000000); + byte[] data = new byte[512]; + file.writeBytes(data, 0, 0, 128); + file.close(); + + try { + file.allowPurging(true); + fail("allowPurging() after close() did not throw IOException."); + } catch (IOException e) { + // this is what should happen + } + } + + // Tests that we don't leak file descriptors or mmap areas + @LargeTest + public void testCloseLeak() throws Exception { + // open enough memory files that we should run out of + // file descriptors or address space if we leak either. + for (int i = 0; i < 1025; i++) { + MemoryFile file = new MemoryFile("MemoryFileTest", 5000000); + file.writeBytes(testString, 0, 0, testString.length); + file.close(); + } + } + + private static final byte[] testString = new byte[] { + 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3, 2, 3, 8, 4, 6, 2, 6, 4, 3, 3, 8, 3, 2, 7, 9, 5, 0, 2, 8, 8, 4, 1, 9, 7, 1, 6, 9, 3, 9, 9, 3, 7, 5, 1, 0, 5, 8, 2, 0, 9, 7, 4, 9, 4, 4, 5, 9, 2, 3, 0, 7, 8, 1, 6, 4, + 0, 6, 2, 8, 6, 2, 0, 8, 9, 9, 8, 6, 2, 8, 0, 3, 4, 8, 2, 5, 3, 4, 2, 1, 1, 7, 0, 6, 7, 9, 8, 2, 1, 4, 8, 0, 8, 6, 5, 1, 3, 2, 8, 2, 3, 0, 6, 6, 4, 7, 0, 9, 3, 8, 4, 4, 6, 0, 9, 5, 5, 0, 5, 8, 2, 2, 3, 1, 7, 2, + 5, 3, 5, 9, 4, 0, 8, 1, 2, 8, 4, 8, 1, 1, 1, 7, 4, 5, 0, 2, 8, 4, 1, 0, 2, 7, 0, 1, 9, 3, 8, 5, 2, 1, 1, 0, 5, 5, 5, 9, 6, 4, 4, 6, 2, 2, 9, 4, 8, 9, 5, 4, 9, 3, 0, 3, 8, 1, 9, 6, 4, 4, 2, 8, 8, 1, 0, 9, 7, 5, + 6, 6, 5, 9, 3, 3, 4, 4, 6, 1, 2, 8, 4, 7, 5, 6, 4, 8, 2, 3, 3, 7, 8, 6, 7, 8, 3, 1, 6, 5, 2, 7, 1, 2, 0, 1, 9, 0, 9, 1, 4, 5, 6, 4, 8, 5, 6, 6, 9, 2, 3, 4, 6, 0, 3, 4, 8, 6, 1, 0, 4, 5, 4, 3, 2, 6, 6, 4, 8, 2, + 1, 3, 3, 9, 3, 6, 0, 7, 2, 6, 0, 2, 4, 9, 1, 4, 1, 2, 7, 3, 7, 2, 4, 5, 8, 7, 0, 0, 6, 6, 0, 6, 3, 1, 5, 5, 8, 8, 1, 7, 4, 8, 8, 1, 5, 2, 0, 9, 2, 0, 9, 6, 2, 8, 2, 9, 2, 5, 4, 0, 9, 1, 7, 1, 5, 3, 6, 4, 3, 6, + 7, 8, 9, 2, 5, 9, 0, 3, 6, 0, 0, 1, 1, 3, 3, 0, 5, 3, 0, 5, 4, 8, 8, 2, 0, 4, 6, 6, 5, 2, 1, 3, 8, 4, 1, 4, 6, 9, 5, 1, 9, 4, 1, 5, 1, 1, 6, 0, 9, 4, 3, 3, 0, 5, 7, 2, 7, 0, 3, 6, 5, 7, 5, 9, 5, 9, 1, 9, 5, 3, + 0, 9, 2, 1, 8, 6, 1, 1, 7, 3, 8, 1, 9, 3, 2, 6, 1, 1, 7, 9, 3, 1, 0, 5, 1, 1, 8, 5, 4, 8, 0, 7, 4, 4, 6, 2, 3, 7, 9, 9, 6, 2, 7, 4, 9, 5, 6, 7, 3, 5, 1, 8, 8, 5, 7, 5, 2, 7, 2, 4, 8, 9, 1, 2, 2, 7, 9, 3, 8, 1, + 8, 3, 0, 1, 1, 9, 4, 9, 1, 2, 9, 8, 3, 3, 6, 7, 3, 3, 6, 2, 4, 4, 0, 6, 5, 6, 6, 4, 3, 0, 8, 6, 0, 2, 1, 3, 9, 4, 9, 4, 6, 3, 9, 5, 2, 2, 4, 7, 3, 7, 1, 9, 0, 7, 0, 2, 1, 7, 9, 8, 6, 0, 9, 4, 3, 7, 0, 2, 7, 7, + 0, 5, 3, 9, 2, 1, 7, 1, 7, 6, 2, 9, 3, 1, 7, 6, 7, 5, 2, 3, 8, 4, 6, 7, 4, 8, 1, 8, 4, 6, 7, 6, 6, 9, 4, 0, 5, 1, 3, 2, 0, 0, 0, 5, 6, 8, 1, 2, 7, 1, 4, 5, 2, 6, 3, 5, 6, 0, 8, 2, 7, 7, 8, 5, 7, 7, 1, 3, 4, 2, + 7, 5, 7, 7, 8, 9, 6, 0, 9, 1, 7, 3, 6, 3, 7, 1, 7, 8, 7, 2, 1, 4, 6, 8, 4, 4, 0, 9, 0, 1, 2, 2, 4, 9, 5, 3, 4, 3, 0, 1, 4, 6, 5, 4, 9, 5, 8, 5, 3, 7, 1, 0, 5, 0, 7, 9, 2, 2, 7, 9, 6, 8, 9, 2, 5, 8, 9, 2, 3, 5, + 4, 2, 0, 1, 9, 9, 5, 6, 1, 1, 2, 1, 2, 9, 0, 2, 1, 9, 6, 0, 8, 6, 4, 0, 3, 4, 4, 1, 8, 1, 5, 9, 8, 1, 3, 6, 2, 9, 7, 7, 4, 7, 7, 1, 3, 0, 9, 9, 6, 0, 5, 1, 8, 7, 0, 7, 2, 1, 1, 3, 4, 9, 9, 9, 9, 9, 9, 8, 3, 7, + 2, 9, 7, 8, 0, 4, 9, 9, 5, 1, 0, 5, 9, 7, 3, 1, 7, 3, 2, 8, 1, 6, 0, 9, 6, 3, 1, 8, 5, 9, 5, 0, 2, 4, 4, 5, 9, 4, 5, 5, 3, 4, 6, 9, 0, 8, 3, 0, 2, 6, 4, 2, 5, 2, 2, 3, 0, 8, 2, 5, 3, 3, 4, 4, 6, 8, 5, 0, 3, 5, + 2, 6, 1, 9, 3, 1, 1, 8, 8, 1, 7, 1, 0, 1, 0, 0, 0, 3, 1, 3, 7, 8, 3, 8, 7, 5, 2, 8, 8, 6, 5, 8, 7, 5, 3, 3, 2, 0, 8, 3, 8, 1, 4, 2, 0, 6, 1, 7, 1, 7, 7, 6, 6, 9, 1, 4, 7, 3, 0, 3, 5, 9, 8, 2, 5, 3, 4, 9, 0, 4, + 2, 8, 7, 5, 5, 4, 6, 8, 7, 3, 1, 1, 5, 9, 5, 6, 2, 8, 6, 3, 8, 8, 2, 3, 5, 3, 7, 8, 7, 5, 9, 3, 7, 5, 1, 9, 5, 7, 7, 8, 1, 8, 5, 7, 7, 8, 0, 5, 3, 2, 1, 7, 1, 2, 2, 6, 8, 0, 6, 6, 1, 3, 0, 0, 1, 9, 2, 7, 8, 7, + 6, 6, 1, 1, 1, 9, 5, 9, 0, 9, 2, 1, 6, 4, 2, 0, 1, 9, 8, 9, 3, 8, 0, 9, 5, 2, 5, 7, 2, 0, 1, 0, 6, 5, 4, 8, 5, 8, 6, 3, 2, 7, 8, 8, 6, 5, 9, 3, 6, 1, 5, 3, 3, 8, 1, 8, 2, 7, 9, 6, 8, 2, 3, 0, 3, 0, 1, 9, 5, 2, + 0, 3, 5, 3, 0, 1, 8, 5, 2, 9, 6, 8, 9, 9, 5, 7, 7, 3, 6, 2, 2, 5, 9, 9, 4, 1, 3, 8, 9, 1, 2, 4, 9, 7, 2, 1, 7, 7, 5, 2, 8, 3, 4, 7, 9, 1, 3, 1, 5, 1, 5, 5, 7, 4, 8, 5, 7, 2, 4, 2, 4, 5, 4, 1, 5, 0, 6, 9, 5, 9, + 5, 0, 8, 2, 9, 5, 3, 3, 1, 1, 6, 8, 6, 1, 7, 2, 7, 8, 5, 5, 8, 8, 9, 0, 7, 5, 0, 9, 8, 3, 8, 1, 7, 5, 4, 6, 3, 7, 4, 6, 4, 9, 3, 9, 3, 1, 9, 2, 5, 5, 0, 6, 0, 4, 0, 0, 9, 2, 7, 7, 0, 1, 6, 7, 1, 1, 3, 9, 0, 0, + 9, 8, 4, 8, 8, 2, 4, 0, 1, 2, 8, 5, 8, 3, 6, 1, 6, 0, 3, 5, 6, 3, 7, 0, 7, 6, 6, 0, 1, 0, 4, 7, 1, 0, 1, 8, 1, 9, 4, 2, 9, 5, 5, 5, 9, 6, 1, 9, 8, 9, 4, 6, 7, 6, 7, 8, 3, 7, 4, 4, 9, 4, 4, 8, 2, 5, 5, 3, 7, 9, + 7, 7, 4, 7, 2, 6, 8, 4, 7, 1, 0, 4, 0, 4, 7, 5, 3, 4, 6, 4, 6, 2, 0, 8, 0, 4, 6, 6, 8, 4, 2, 5, 9, 0, 6, 9, 4, 9, 1, 2, 9, 3, 3, 1, 3, 6, 7, 7, 0, 2, 8, 9, 8, 9, 1, 5, 2, 1, 0, 4, 7, 5, 2, 1, 6, 2, 0, 5, 6, 9, + 6, 6, 0, 2, 4, 0, 5, 8, 0, 3, 8, 1, 5, 0, 1, 9, 3, 5, 1, 1, 2, 5, 3, 3, 8, 2, 4, 3, 0, 0, 3, 5, 5, 8, 7, 6, 4, 0, 2, 4, 7, 4, 9, 6, 4, 7, 3, 2, 6, 3, 9, 1, 4, 1, 9, 9, 2, 7, 2, 6, 0, 4, 2, 6, 9, 9, 2, 2, 7, 9, + 6, 7, 8, 2, 3, 5, 4, 7, 8, 1, 6, 3, 6, 0, 0, 9, 3, 4, 1, 7, 2, 1, 6, 4, 1, 2, 1, 9, 9, 2, 4, 5, 8, 6, 3, 1, 5, 0, 3, 0, 2, 8, 6, 1, 8, 2, 9, 7, 4, 5, 5, 5, 7, 0, 6, 7, 4, 9, 8, 3, 8, 5, 0, 5, 4, 9, 4, 5, 8, 8, + 5, 8, 6, 9, 2, 6, 9, 9, 5, 6, 9, 0, 9, 2, 7, 2, 1, 0, 7, 9, 7, 5, 0, 9, 3, 0, 2, 9, 5, 5, 3, 2, 1, 1, 6, 5, 3, 4, 4, 9, 8, 7, 2, 0, 2, 7, 5, 5, 9, 6, 0, 2, 3, 6, 4, 8, 0, 6, 6, 5, 4, 9, 9, 1, 1, 9, 8, 8, 1, 8, + 3, 4, 7, 9, 7, 7, 5, 3, 5, 6, 6, 3, 6, 9, 8, 0, 7, 4, 2, 6, 5, 4, 2, 5, 2, 7, 8, 6, 2, 5, 5, 1, 8, 1, 8, 4, 1, 7, 5, 7, 4, 6, 7, 2, 8, 9, 0, 9, 7, 7, 7, 7, 2, 7, 9, 3, 8, 0, 0, 0, 8, 1, 6, 4, 7, 0, 6, 0, 0, 1, + 6, 1, 4, 5, 2, 4, 9, 1, 9, 2, 1, 7, 3, 2, 1, 7, 2, 1, 4, 7, 7, 2, 3, 5, 0, 1, 4, 1, 4, 4, 1, 9, 7, 3, 5, 6, 8, 5, 4, 8, 1, 6, 1, 3, 6, 1, 1, 5, 7, 3, 5, 2, 5, 5, 2, 1, 3, 3, 4, 7, 5, 7, 4, 1, 8, 4, 9, 4, 6, 8, + 4, 3, 8, 5, 2, 3, 3, 2, 3, 9, 0, 7, 3, 9, 4, 1, 4, 3, 3, 3, 4, 5, 4, 7, 7, 6, 2, 4, 1, 6, 8, 6, 2, 5, 1, 8, 9, 8, 3, 5, 6, 9, 4, 8, 5, 5, 6, 2, 0, 9, 9, 2, 1, 9, 2, 2, 2, 1, 8, 4, 2, 7, 2, 5, 5, 0, 2, 5, 4, 2, + 5, 6, 8, 8, 7, 6, 7, 1, 7, 9, 0, 4, 9, 4, 6, 0, 1, 6, 5, 3, 4, 6, 6, 8, 0, 4, 9, 8, 8, 6, 2, 7, 2, 3, 2, 7, 9, 1, 7, 8, 6, 0, 8, 5, 7, 8, 4, 3, 8, 3, 8, 2, 7, 9, 6, 7, 9, 7, 6, 6, 8, 1, 4, 5, 4, 1, 0, 0, 9, 5, + 3, 8, 8, 3, 7, 8, 6, 3, 6, 0, 9, 5, 0, 6, 8, 0, 0, 6, 4, 2, 2, 5, 1, 2, 5, 2, 0, 5, 1, 1, 7, 3, 9, 2, 9, 8, 4, 8, 9, 6, 0, 8, 4, 1, 2, 8, 4, 8, 8, 6, 2, 6, 9, 4, 5, 6, 0, 4, 2, 4, 1, 9, 6, 5, 2, 8, 5, 0, 2, 2, + 2, 1, 0, 6, 6, 1, 1, 8, 6, 3, 0, 6, 7, 4, 4, 2, 7, 8, 6, 2, 2, 0, 3, 9, 1, 9, 4, 9, 4, 5, 0, 4, 7, 1, 2, 3, 7, 1, 3, 7, 8, 6, 9, 6, 0, 9, 5, 6, 3, 6, 4, 3, 7, 1, 9, 1, 7, 2, 8, 7, 4, 6, 7, 7, 6, 4, 6, 5, 7, 5, + 7, 3, 9, 6, 2, 4, 1, 3, 8, 9, 0, 8, 6, 5, 8, 3, 2, 6, 4, 5, 9, 9, 5, 8, 1, 3, 3, 9, 0, 4, 7, 8, 0, 2, 7, 5, 9, 0, 0, 9, 9, 4, 6, 5, 7, 6, 4, 0, 7, 8, 9, 5, 1, 2, 6, 9, 4, 6, 8, 3, 9, 8, 3, 5, 2, 5, 9, 5, 7, 0, + 9, 8, 2, 5, 8, 2, 2, 6, 2, 0, 5, 2, 2, 4, 8, 9, 4, 0, 7, 7, 2, 6, 7, 1, 9, 4, 7, 8, 2, 6, 8, 4, 8, 2, 6, 0, 1, 4, 7, 6, 9, 9, 0, 9, 0, 2, 6, 4, 0, 1, 3, 6, 3, 9, 4, 4, 3, 7, 4, 5, 5, 3, 0, 5, 0, 6, 8, 2, 0, 3, + 4, 9, 6, 2, 5, 2, 4, 5, 1, 7, 4, 9, 3, 9, 9, 6, 5, 1, 4, 3, 1, 4, 2, 9, 8, 0, 9, 1, 9, 0, 6, 5, 9, 2, 5, 0, 9, 3, 7, 2, 2, 1, 6, 9, 6, 4, 6, 1, 5, 1, 5, 7, 0, 9, 8, 5, 8, 3, 8, 7, 4, 1, 0, 5, 9, 7, 8, 8, 5, 9, + 5, 9, 7, 7, 2, 9, 7, 5, 4, 9, 8, 9, 3, 0, 1, 6, 1, 7, 5, 3, 9, 2, 8, 4, 6, 8, 1, 3, 8, 2, 6, 8, 6, 8, 3, 8, 6, 8, 9, 4, 2, 7, 7, 4, 1, 5, 5, 9, 9, 1, 8, 5, 5, 9, 2, 5, 2, 4, 5, 9, 5, 3, 9, 5, 9, 4, 3, 1, 0, 4, + 9, 9, 7, 2, 5, 2, 4, 6, 8, 0, 8, 4, 5, 9, 8, 7, 2, 7, 3, 6, 4, 4, 6, 9, 5, 8, 4, 8, 6, 5, 3, 8, 3, 6, 7, 3, 6, 2, 2, 2, 6, 2, 6, 0, 9, 9, 1, 2, 4, 6, 0, 8, 0, 5, 1, 2, 4, 3, 8, 8, 4, 3, 9, 0, 4, 5, 1, 2, 4, 4, + 1, 3, 6, 5, 4, 9, 7, 6, 2, 7, 8, 0, 7, 9, 7, 7, 1, 5, 6, 9, 1, 4, 3, 5, 9, 9, 7, 7, 0, 0, 1, 2, 9, 6, 1, 6, 0, 8, 9, 4, 4, 1, 6, 9, 4, 8, 6, 8, 5, 5, 5, 8, 4, 8, 4, 0, 6, 3, 5, 3, 4, 2, 2, 0, 7, 2, 2, 2, 5, 8, + 2, 8, 4, 8, 8, 6, 4, 8, 1, 5, 8, 4, 5, 6, 0, 2, 8, 5, 0, 6, 0, 1, 6, 8, 4, 2, 7, 3, 9, 4, 5, 2, 2, 6, 7, 4, 6, 7, 6, 7, 8, 8, 9, 5, 2, 5, 2, 1, 3, 8, 5, 2, 2, 5, 4, 9, 9, 5, 4, 6, 6, 6, 7, 2, 7, 8, 2, 3, 9, 8, + 6, 4, 5, 6, 5, 9, 6, 1, 1, 6, 3, 5, 4, 8, 8, 6, 2, 3, 0, 5, 7, 7, 4, 5, 6, 4, 9, 8, 0, 3, 5, 5, 9, 3, 6, 3, 4, 5, 6, 8, 1, 7, 4, 3, 2, 4, 1, 1, 2, 5, 1, 5, 0, 7, 6, 0, 6, 9, 4, 7, 9, 4, 5, 1, 0, 9, 6, 5, 9, 6, + 0, 9, 4, 0, 2, 5, 2, 2, 8, 8, 7, 9, 7, 1, 0, 8, 9, 3, 1, 4, 5, 6, 6, 9, 1, 3, 6, 8, 6, 7, 2, 2, 8, 7, 4, 8, 9, 4, 0, 5, 6, 0, 1, 0, 1, 5, 0, 3, 3, 0, 8, 6, 1, 7, 9, 2, 8, 6, 8, 0, 9, 2, 0, 8, 7, 4, 7, 6, 0, 9, + 1, 7, 8, 2, 4, 9, 3, 8, 5, 8, 9, 0, 0, 9, 7, 1, 4, 9, 0, 9, 6, 7, 5, 9, 8, 5, 2, 6, 1, 3, 6, 5, 5, 4, 9, 7, 8, 1, 8, 9, 3, 1, 2, 9, 7, 8, 4, 8, 2, 1, 6, 8, 2, 9, 9, 8, 9, 4, 8, 7, 2, 2, 6, 5, 8, 8, 0, 4, 8, 5, + 7, 5, 6, 4, 0, 1, 4, 2, 7, 0, 4, 7, 7, 5, 5, 5, 1, 3, 2, 3, 7, 9, 6, 4, 1, 4, 5, 1, 5, 2, 3, 7, 4, 6, 2, 3, 4, 3, 6, 4, 5, 4, 2, 8, 5, 8, 4, 4, 4, 7, 9, 5, 2, 6, 5, 8, 6, 7, 8, 2, 1, 0, 5, 1, 1, 4, 1, 3, 5, 4, + 7, 3, 5, 7, 3, 9, 5, 2, 3, 1, 1, 3, 4, 2, 7, 1, 6, 6, 1, 0, 2, 1, 3, 5, 9, 6, 9, 5, 3, 6, 2, 3, 1, 4, 4, 2, 9, 5, 2, 4, 8, 4, 9, 3, 7, 1, 8, 7, 1, 1, 0, 1, 4, 5, 7, 6, 5, 4, 0, 3, 5, 9, 0, 2, 7, 9, 9, 3, 4, 4, + 0, 3, 7, 4, 2, 0, 0, 7, 3, 1, 0, 5, 7, 8, 5, 3, 9, 0, 6, 2, 1, 9, 8, 3, 8, 7, 4, 4, 7, 8, 0, 8, 4, 7, 8, 4, 8, 9, 6, 8, 3, 3, 2, 1, 4, 4, 5, 7, 1, 3, 8, 6, 8, 7, 5, 1, 9, 4, 3, 5, 0, 6, 4, 3, 0, 2, 1, 8, 4, 5, + 3, 1, 9, 1, 0, 4, 8, 4, 8, 1, 0, 0, 5, 3, 7, 0, 6, 1, 4, 6, 8, 0, 6, 7, 4, 9, 1, 9, 2, 7, 8, 1, 9, 1, 1, 9, 7, 9, 3, 9, 9, 5, 2, 0, 6, 1, 4, 1, 9, 6, 6, 3, 4, 2, 8, 7, 5, 4, 4, 4, 0, 6, 4, 3, 7, 4, 5, 1, 2, 3, + 7, 1, 8, 1, 9, 2, 1, 7, 9, 9, 9, 8, 3, 9, 1, 0, 1, 5, 9, 1, 9, 5, 6, 1, 8, 1, 4, 6, 7, 5, 1, 4, 2, 6, 9, 1, 2, 3, 9, 7, 4, 8, 9, 4, 0, 9, 0, 7, 1, 8, 6, 4, 9, 4, 2, 3, 1, 9, 6, 1, 5, 6, 7, 9, 4, 5, 2, 0, 8, 0, + 9, 5, 1, 4, 6, 5, 5, 0, 2, 2, 5, 2, 3, 1, 6, 0, 3, 8, 8, 1, 9, 3, 0, 1, 4, 2, 0, 9, 3, 7, 6, 2, 1, 3, 7, 8, 5, 5, 9, 5, 6, 6, 3, 8, 9, 3, 7, 7, 8, 7, 0, 8, 3, 0, 3, 9, 0, 6, 9, 7, 9, 2, 0, 7, 7, 3, 4, 6, 7, 2, + 2, 1, 8, 2, 5, 6, 2, 5, 9, 9, 6, 6, 1, 5, 0, 1, 4, 2, 1, 5, 0, 3, 0, 6, 8, 0, 3, 8, 4, 4, 7, 7, 3, 4, 5, 4, 9, 2, 0, 2, 6, 0, 5, 4, 1, 4, 6, 6, 5, 9, 2, 5, 2, 0, 1, 4, 9, 7, 4, 4, 2, 8, 5, 0, 7, 3, 2, 5, 1, 8, + 6, 6, 6, 0, 0, 2, 1, 3, 2, 4, 3, 4, 0, 8, 8, 1, 9, 0, 7, 1, 0, 4, 8, 6, 3, 3, 1, 7, 3, 4, 6, 4, 9, 6, 5, 1, 4, 5, 3, 9, 0, 5, 7, 9, 6, 2, 6, 8, 5, 6, 1, 0, 0, 5, 5, 0, 8, 1, 0, 6, 6, 5, 8, 7, 9, 6, 9, 9, 8, 1, + 6, 3, 5, 7, 4, 7, 3, 6, 3, 8, 4, 0, 5, 2, 5, 7, 1, 4, 5, 9, 1, 0, 2, 8, 9, 7, 0, 6, 4, 1, 4, 0, 1, 1, 0, 9, 7, 1, 2, 0, 6, 2, 8, 0, 4, 3, 9, 0, 3, 9, 7, 5, 9, 5, 1, 5, 6, 7, 7, 1, 5, 7, 7, 0, 0, 4, 2, 0, 3, 3, + 7, 8, 6, 9, 9, 3, 6, 0, 0, 7, 2, 3, 0, 5, 5, 8, 7, 6, 3, 1, 7, 6, 3, 5, 9, 4, 2, 1, 8, 7, 3, 1, 2, 5, 1, 4, 7, 1, 2, 0, 5, 3, 2, 9, 2, 8, 1, 9, 1, 8, 2, 6, 1, 8, 6, 1, 2, 5, 8, 6, 7, 3, 2, 1, 5, 7, 9, 1, 9, 8, + 4, 1, 4, 8, 4, 8, 8, 2, 9, 1, 6, 4, 4, 7, 0, 6, 0, 9, 5, 7, 5, 2, 7, 0, 6, 9, 5, 7, 2, 2, 0, 9, 1, 7, 5, 6, 7, 1, 1, 6, 7, 2, 2, 9, 1, 0, 9, 8, 1, 6, 9, 0, 9, 1, 5, 2, 8, 0, 1, 7, 3, 5, 0, 6, 7, 1, 2, 7, 4, 8, + 5, 8, 3, 2, 2, 2, 8, 7, 1, 8, 3, 5, 2, 0, 9, 3, 5, 3, 9, 6, 5, 7, 2, 5, 1, 2, 1, 0, 8, 3, 5, 7, 9, 1, 5, 1, 3, 6, 9, 8, 8, 2, 0, 9, 1, 4, 4, 4, 2, 1, 0, 0, 6, 7, 5, 1, 0, 3, 3, 4, 6, 7, 1, 1, 0, 3, 1, 4, 1, 2, + 6, 7, 1, 1, 1, 3, 6, 9, 9, 0, 8, 6, 5, 8, 5, 1, 6, 3, 9, 8, 3, 1, 5, 0, 1, 9, 7, 0, 1, 6, 5, 1, 5, 1, 1, 6, 8, 5, 1, 7, 1, 4, 3, 7, 6, 5, 7, 6, 1, 8, 3, 5, 1, 5, 5, 6, 5, 0, 8, 8, 4, 9, 0, 9, 9, 8, 9, 8, 5, 9, + 9, 8, 2, 3, 8, 7, 3, 4, 5, 5, 2, 8, 3, 3, 1, 6, 3, 5, 5, 0, 7, 6, 4, 7, 9, 1, 8, 5, 3, 5, 8, 9, 3, 2, 2, 6, 1, 8, 5, 4, 8, 9, 6, 3, 2, 1, 3, 2, 9, 3, 3, 0, 8, 9, 8, 5, 7, 0, 6, 4, 2, 0, 4, 6, 7, 5, 2, 5, 9, 0, + 7, 0, 9, 1, 5, 4, 8, 1, 4, 1, 6, 5, 4, 9, 8, 5, 9, 4, 6, 1, 6, 3, 7, 1, 8, 0, 2, 7, 0, 9, 8, 1, 9, 9, 4, 3, 0, 9, 9, 2, 4, 4, 8, 8, 9, 5, 7, 5, 7, 1, 2, 8, 2, 8, 9, 0, 5, 9, 2, 3, 2, 3, 3, 2, 6, 0, 9, 7, 2, 9, + 9, 7, 1, 2, 0, 8, 4, 4, 3, 3, 5, 7, 3, 2, 6, 5, 4, 8, 9, 3, 8, 2, 3, 9, 1, 1, 9, 3, 2, 5, 9, 7, 4, 6, 3, 6, 6, 7, 3, 0, 5, 8, 3, 6, 0, 4, 1, 4, 2, 8, 1, 3, 8, 8, 3, 0, 3, 2, 0, 3, 8, 2, 4, 9, 0, 3, 7, 5, 8, 9, + 8, 5, 2, 4, 3, 7, 4, 4, 1, 7, 0, 2, 9, 1, 3, 2, 7, 6, 5, 6, 1, 8, 0, 9, 3, 7, 7, 3, 4, 4, 4, 0, 3, 0, 7, 0, 7, 4, 6, 9, 2, 1, 1, 2, 0, 1, 9, 1, 3, 0, 2, 0, 3, 3, 0, 3, 8, 0, 1, 9, 7, 6, 2, 1, 1, 0, 1, 1, 0, 0, + 4, 4, 9, 2, 9, 3, 2, 1, 5, 1, 6, 0, 8, 4, 2, 4, 4, 4, 8, 5, 9, 6, 3, 7, 6, 6, 9, 8, 3, 8, 9, 5, 2, 2, 8, 6, 8, 4, 7, 8, 3, 1, 2, 3, 5, 5, 2, 6, 5, 8, 2, 1, 3, 1, 4, 4, 9, 5, 7, 6, 8, 5, 7, 2, 6, 2, 4, 3, 3, 4, + 4, 1, 8, 9, 3, 0, 3, 9, 6, 8, 6, 4, 2, 6, 2, 4, 3, 4, 1, 0, 7, 7, 3, 2, 2, 6, 9, 7, 8, 0, 2, 8, 0, 7, 3, 1, 8, 9, 1, 5, 4, 4, 1, 1, 0, 1, 0, 4, 4, 6, 8, 2, 3, 2, 5, 2, 7, 1, 6, 2, 0, 1, 0, 5, 2, 6, 5, 2, 2, 7, + 2, 1, 1, 1, 6, 6, 0, 3, 9, 6, 6, 6, 5, 5, 7, 3, 0, 9, 2, 5, 4, 7, 1, 1, 0, 5, 5, 7, 8, 5, 3, 7, 6, 3, 4, 6, 6, 8, 2, 0, 6, 5, 3, 1, 0, 9, 8, 9, 6, 5, 2, 6, 9, 1, 8, 6, 2, 0, 5, 6, 4, 7, 6, 9, 3, 1, 2, 5, 7, 0, + 5, 8, 6, 3, 5, 6, 6, 2, 0, 1, 8, 5, 5, 8, 1, 0, 0, 7, 2, 9, 3, 6, 0, 6, 5, 9, 8, 7, 6, 4, 8, 6, 1, 1, 7, 9, 1, 0, 4, 5, 3, 3, 4, 8, 8, 5, 0, 3, 4, 6, 1, 1, 3, 6, 5, 7, 6, 8, 6, 7, 5, 3, 2, 4, 9, 4, 4, 1, 6, 6, + 8, 0, 3, 9, 6, 2, 6, 5, 7, 9, 7, 8, 7, 7, 1, 8, 5, 5, 6, 0, 8, 4, 5, 5, 2, 9, 6, 5, 4, 1, 2, 6, 6, 5, 4, 0, 8, 5, 3, 0, 6, 1, 4, 3, 4, 4, 4, 3, 1, 8, 5, 8, 6, 7, 6, 9, 7, 5, 1, 4, 5, 6, 6, 1, 4, 0, 6, 8, 0, 0, + 7, 0, 0, 2, 3, 7, 8, 7, 7, 6, 5, 9, 1, 3, 4, 4, 0, 1, 7, 1, 2, 7, 4, 9, 4, 7, 0, 4, 2, 0, 5, 6, 2, 2, 3, 0, 5, 3, 8, 9, 9, 4, 5, 6, 1, 3, 1, 4, 0, 7, 1, 1, 2, 7, 0, 0, 0, 4, 0, 7, 8, 5, 4, 7, 3, 3, 2, 6, 9, 9, + 3, 9, 0, 8, 1, 4, 5, 4, 6, 6, 4, 6, 4, 5, 8, 8, 0, 7, 9, 7, 2, 7, 0, 8, 2, 6, 6, 8, 3, 0, 6, 3, 4, 3, 2, 8, 5, 8, 7, 8, 5, 6, 9, 8, 3, 0, 5, 2, 3, 5, 8, 0, 8, 9, 3, 3, 0, 6, 5, 7, 5, 7, 4, 0, 6, 7, 9, 5, 4, 5, + 7, 1, 6, 3, 7, 7, 5, 2, 5, 4, 2, 0, 2, 1, 1, 4, 9, 5, 5, 7, 6, 1, 5, 8, 1, 4, 0, 0, 2, 5, 0, 1, 2, 6, 2, 2, 8, 5, 9, 4, 1, 3, 0, 2, 1, 6, 4, 7, 1, 5, 5, 0, 9, 7, 9, 2, 5, 9, 2, 3, 0, 9, 9, 0, 7, 9, 6, 5, 4, 7, + 3, 7, 6, 1, 2, 5, 5, 1, 7, 6, 5, 6, 7, 5, 1, 3, 5, 7, 5, 1, 7, 8, 2, 9, 6, 6, 6, 4, 5, 4, 7, 7, 9, 1, 7, 4, 5, 0, 1, 1, 2, 9, 9, 6, 1, 4, 8, 9, 0, 3, 0, 4, 6, 3, 9, 9, 4, 7, 1, 3, 2, 9, 6, 2, 1, 0, 7, 3, 4, 0, + 4, 3, 7, 5, 1, 8, 9, 5, 7, 3, 5, 9, 6, 1, 4, 5, 8, 9, 0, 1, 9, 3, 8, 9, 7, 1, 3, 1, 1, 1, 7, 9, 0, 4, 2, 9, 7, 8, 2, 8, 5, 6, 4, 7, 5, 0, 3, 2, 0, 3, 1, 9, 8, 6, 9, 1, 5, 1, 4, 0, 2, 8, 7, 0, 8, 0, 8, 5, 9, 9, + 0, 4, 8, 0, 1, 0, 9, 4, 1, 2, 1, 4, 7, 2, 2, 1, 3, 1, 7, 9, 4, 7, 6, 4, 7, 7, 7, 2, 6, 2, 2, 4, 1, 4, 2, 5, 4, 8, 5, 4, 5, 4, 0, 3, 3, 2, 1, 5, 7, 1, 8, 5, 3, 0, 6, 1, 4, 2, 2, 8, 8, 1, 3, 7, 5, 8, 5, 0, 4, 3, + 0, 6, 3, 3, 2, 1, 7, 5, 1, 8, 2, 9, 7, 9, 8, 6, 6, 2, 2, 3, 7, 1, 7, 2, 1, 5, 9, 1, 6, 0, 7, 7, 1, 6, 6, 9, 2, 5, 4, 7, 4, 8, 7, 3, 8, 9, 8, 6, 6, 5, 4, 9, 4, 9, 4, 5, 0, 1, 1, 4, 6, 5, 4, 0, 6, 2, 8, 4, 3, 3, + 6, 6, 3, 9, 3, 7, 9, 0, 0, 3, 9, 7, 6, 9, 2, 6, 5, 6, 7, 2, 1, 4, 6, 3, 8, 5, 3, 0, 6, 7, 3, 6, 0, 9, 6, 5, 7, 1, 2, 0, 9, 1, 8, 0, 7, 6, 3, 8, 3, 2, 7, 1, 6, 6, 4, 1, 6, 2, 7, 4, 8, 8, 8, 8, 0, 0, 7, 8, 6, 9, + 2, 5, 6, 0, 2, 9, 0, 2, 2, 8, 4, 7, 2, 1, 0, 4, 0, 3, 1, 7, 2, 1, 1, 8, 6, 0, 8, 2, 0, 4, 1, 9, 0, 0, 0, 4, 2, 2, 9, 6, 6, 1, 7, 1, 1, 9, 6, 3, 7, 7, 9, 2, 1, 3, 3, 7, 5, 7, 5, 1, 1, 4, 9, 5, 9, 5, 0, 1, 5, 6, + 6, 0, 4, 9, 6, 3, 1, 8, 6, 2, 9, 4, 7, 2, 6, 5, 4, 7, 3, 6, 4, 2, 5, 2, 3, 0, 8, 1, 7, 7, 0, 3, 6, 7, 5, 1, 5, 9, 0, 6, 7, 3, 5, 0, 2, 3, 5, 0, 7, 2, 8, 3, 5, 4, 0, 5, 6, 7, 0, 4, 0, 3, 8, 6, 7, 4, 3, 5, 1, 3, + 6, 2, 2, 2, 2, 4, 7, 7, 1, 5, 8, 9, 1, 5, 0, 4, 9, 5, 3, 0, 9, 8, 4, 4, 4, 8, 9, 3, 3, 3, 0, 9, 6, 3, 4, 0, 8, 7, 8, 0, 7, 6, 9, 3, 2, 5, 9, 9, 3, 9, 7, 8, 0, 5, 4, 1, 9, 3, 4, 1, 4, 4, 7, 3, 7, 7, 4, 4, 1, 8, + 4, 2, 6, 3, 1, 2, 9, 8, 6, 0, 8, 0, 9, 9, 8, 8, 8, 6, 8, 7, 4, 1, 3, 2, 6, 0, 4, 7, 2, 1, 5, 6, 9, 5, 1, 6, 2, 3, 9, 6, 5, 8, 6, 4, 5, 7, 3, 0, 2, 1, 6, 3, 1, 5, 9, 8, 1, 9, 3, 1, 9, 5, 1, 6, 7, 3, 5, 3, 8, 1, + 2, 9, 7, 4, 1, 6, 7, 7, 2, 9, 4, 7, 8, 6, 7, 2, 4, 2, 2, 9, 2, 4, 6, 5, 4, 3, 6, 6, 8, 0, 0, 9, 8, 0, 6, 7, 6, 9, 2, 8, 2, 3, 8, 2, 8, 0, 6, 8, 9, 9, 6, 4, 0, 0, 4, 8, 2, 4, 3, 5, 4, 0, 3, 7, 0, 1, 4, 1, 6, 3, + 1, 4, 9, 6, 5, 8, 9, 7, 9, 4, 0, 9, 2, 4, 3, 2, 3, 7, 8, 9, 6, 9, 0, 7, 0, 6, 9, 7, 7, 9, 4, 2, 2, 3, 6, 2, 5, 0, 8, 2, 2, 1, 6, 8, 8, 9, 5, 7, 3, 8, 3, 7, 9, 8, 6, 2, 3, 0, 0, 1, 5, 9, 3, 7, 7, 6, 4, 7, 1, 6, + 5, 1, 2, 2, 8, 9, 3, 5, 7, 8, 6, 0, 1, 5, 8, 8, 1, 6, 1, 7, 5, 5, 7, 8, 2, 9, 7, 3, 5, 2, 3, 3, 4, 4, 6, 0, 4, 2, 8, 1, 5, 1, 2, 6, 2, 7, 2, 0, 3, 7, 3, 4, 3, 1, 4, 6, 5, 3, 1, 9, 7, 7, 7, 7, 4, 1, 6, 0, 3, 1, + 9, 9, 0, 6, 6, 5, 5, 4, 1, 8, 7, 6, 3, 9, 7, 9, 2, 9, 3, 3, 4, 4, 1, 9, 5, 2, 1, 5, 4, 1, 3, 4, 1, 8, 9, 9, 4, 8, 5, 4, 4, 4, 7, 3, 4, 5, 6, 7, 3, 8, 3, 1, 6, 2, 4, 9, 9, 3, 4, 1, 9, 1, 3, 1, 8, 1, 4, 8, 0, 9, + 2, 7, 7, 7, 7, 1, 0, 3, 8, 6, 3, 8, 7, 7, 3, 4, 3, 1, 7, 7, 2, 0, 7, 5, 4, 5, 6, 5, 4, 5, 3, 2, 2, 0, 7, 7, 7, 0, 9, 2, 1, 2, 0, 1, 9, 0, 5, 1, 6, 6, 0, 9, 6, 2, 8, 0, 4, 9, 0, 9, 2, 6, 3, 6, 0, 1, 9, 7, 5, 9, + 8, 8, 2, 8, 1, 6, 1, 3, 3, 2, 3, 1, 6, 6, 6, 3, 6, 5, 2, 8, 6, 1, 9, 3, 2, 6, 6, 8, 6, 3, 3, 6, 0, 6, 2, 7, 3, 5, 6, 7, 6, 3, 0, 3, 5, 4, 4, 7, 7, 6, 2, 8, 0, 3, 5, 0, 4, 5, 0, 7, 7, 7, 2, 3, 5, 5, 4, 7, 1, 0, + 5, 8, 5, 9, 5, 4, 8, 7, 0, 2, 7, 9, 0, 8, 1, 4, 3, 5, 6, 2, 4, 0, 1, 4, 5, 1, 7, 1, 8, 0, 6, 2, 4, 6, 4, 3, 6, 2, 6, 7, 9, 4, 5, 6, 1, 2, 7, 5, 3, 1, 8, 1, 3, 4, 0, 7, 8, 3, 3, 0, 3, 3, 6, 2, 5, 4, 2, 3, 2, 7, + 8, 3, 9, 4, 4, 9, 7, 5, 3, 8, 2, 4, 3, 7, 2, 0, 5, 8, 3, 5, 3, 1, 1, 4, 7, 7, 1, 1, 9, 9, 2, 6, 0, 6, 3, 8, 1, 3, 3, 4, 6, 7, 7, 6, 8, 7, 9, 6, 9, 5, 9, 7, 0, 3, 0, 9, 8, 3, 3, 9, 1, 3, 0, 7, 7, 1, 0, 9, 8, 7, + 0, 4, 0, 8, 5, 9, 1, 3, 3, 7, 4, 6, 4, 1, 4, 4, 2, 8, 2, 2, 7, 7, 2, 6, 3, 4, 6, 5, 9, 4, 7, 0, 4, 7, 4, 5, 8, 7, 8, 4, 7, 7, 8, 7, 2, 0, 1, 9, 2, 7, 7, 1, 5, 2, 8, 0, 7, 3, 1, 7, 6, 7, 9, 0, 7, 7, 0, 7, 1, 5, + 7, 2, 1, 3, 4, 4, 4, 7, 3, 0, 6, 0, 5, 7, 0, 0, 7, 3, 3, 4, 9, 2, 4, 3, 6, 9, 3, 1, 1, 3, 8, 3, 5, 0, 4, 9, 3, 1, 6, 3, 1, 2, 8, 4, 0, 4, 2, 5, 1, 2, 1, 9, 2, 5, 6, 5, 1, 7, 9, 8, 0, 6, 9, 4, 1, 1, 3, 5, 2, 8, + 0, 1, 3, 1, 4, 7, 0, 1, 3, 0, 4, 7, 8, 1, 6, 4, 3, 7, 8, 8, 5, 1, 8, 5, 2, 9, 0, 9, 2, 8, 5, 4, 5, 2, 0, 1, 1, 6, 5, 8, 3, 9, 3, 4, 1, 9, 6, 5, 6, 2, 1, 3, 4, 9, 1, 4, 3, 4, 1, 5, 9, 5, 6, 2, 5, 8, 6, 5, 8, 6, + 5, 5, 7, 0, 5, 5, 2, 6, 9, 0, 4, 9, 6, 5, 2, 0, 9, 8, 5, 8, 0, 3, 3, 8, 5, 0, 7, 2, 2, 4, 2, 6, 4, 8, 2, 9, 3, 9, 7, 2, 8, 5, 8, 4, 7, 8, 3, 1, 6, 3, 0, 5, 7, 7, 7, 7, 5, 6, 0, 6, 8, 8, 8, 7, 6, 4, 4, 6, 2, 4, + 8, 2, 4, 6, 8, 5, 7, 9, 2, 6, 0, 3, 9, 5, 3, 5, 2, 7, 7, 3, 4, 8, 0, 3, 0, 4, 8, 0, 2, 9, 0, 0, 5, 8, 7, 6, 0, 7, 5, 8, 2, 5, 1, 0, 4, 7, 4, 7, 0, 9, 1, 6, 4, 3, 9, 6, 1, 3, 6, 2, 6, 7, 6, 0, 4, 4, 9, 2, 5, 6, + 2, 7, 4, 2, 0, 4, 2, 0, 8, 3, 2, 0, 8, 5, 6, 6, 1, 1, 9, 0, 6, 2, 5, 4, 5, 4, 3, 3, 7, 2, 1, 3, 1, 5, 3, 5, 9, 5, 8, 4, 5, 0, 6, 8, 7, 7, 2, 4, 6, 0, 2, 9, 0, 1, 6, 1, 8, 7, 6, 6, 7, 9, 5, 2, 4, 0, 6, 1, 6, 3, + 4, 2, 5, 2, 2, 5, 7, 7, 1, 9, 5, 4, 2, 9, 1, 6, 2, 9, 9, 1, 9, 3, 0, 6, 4, 5, 5, 3, 7, 7, 9, 9, 1, 4, 0, 3, 7, 3, 4, 0, 4, 3, 2, 8, 7, 5, 2, 6, 2, 8, 8, 8, 9, 6, 3, 9, 9, 5, 8, 7, 9, 4, 7, 5, 7, 2, 9, 1, 7, 4, + 6, 4, 2, 6, 3, 5, 7, 4, 5, 5, 2, 5, 4, 0, 7, 9, 0, 9, 1, 4, 5, 1, 3, 5, 7, 1, 1, 1, 3, 6, 9, 4, 1, 0, 9, 1, 1, 9, 3, 9, 3, 2, 5, 1, 9, 1, 0, 7, 6, 0, 2, 0, 8, 2, 5, 2, 0, 2, 6, 1, 8, 7, 9, 8, 5, 3, 1, 8, 8, 7, + 7, 0, 5, 8, 4, 2, 9, 7, 2, 5, 9, 1, 6, 7, 7, 8, 1, 3, 1, 4, 9, 6, 9, 9, 0, 0, 9, 0, 1, 9, 2, 1, 1, 6, 9, 7, 1, 7, 3, 7, 2, 7, 8, 4, 7, 6, 8, 4, 7, 2, 6, 8, 6, 0, 8, 4, 9, 0, 0, 3, 3, 7, 7, 0, 2, 4, 2, 4, 2, 9, + 1, 6, 5, 1, 3, 0, 0, 5, 0, 0, 5, 1, 6, 8, 3, 2, 3, 3, 6, 4, 3, 5, 0, 3, 8, 9, 5, 1, 7, 0, 2, 9, 8, 9, 3, 9, 2, 2, 3, 3, 4, 5, 1, 7, 2, 2, 0, 1, 3, 8, 1, 2, 8, 0, 6, 9, 6, 5, 0, 1, 1, 7, 8, 4, 4, 0, 8, 7, 4, 5, + 1, 9, 6, 0, 1, 2, 1, 2, 2, 8, 5, 9, 9, 3, 7, 1, 6, 2, 3, 1, 3, 0, 1, 7, 1, 1, 4, 4, 4, 8, 4, 6, 4, 0, 9, 0, 3, 8, 9, 0, 6, 4, 4, 9, 5, 4, 4, 4, 0, 0, 6, 1, 9, 8, 6, 9, 0, 7, 5, 4, 8, 5, 1, 6, 0, 2, 6, 3, 2, 7, + 5, 0, 5, 2, 9, 8, 3, 4, 9, 1, 8, 7, 4, 0, 7, 8, 6, 6, 8, 0, 8, 8, 1, 8, 3, 3, 8, 5, 1, 0, 2, 2, 8, 3, 3, 4, 5, 0, 8, 5, 0, 4, 8, 6, 0, 8, 2, 5, 0, 3, 9, 3, 0, 2, 1, 3, 3, 2, 1, 9, 7, 1, 5, 5, 1, 8, 4, 3, 0, 6, + 3, 5, 4, 5, 5, 0, 0, 7, 6, 6, 8, 2, 8, 2, 9, 4, 9, 3, 0, 4, 1, 3, 7, 7, 6, 5, 5, 2, 7, 9, 3, 9, 7, 5, 1, 7, 5, 4, 6, 1, 3, 9, 5, 3, 9, 8, 4, 6, 8, 3, 3, 9, 3, 6, 3, 8, 3, 0, 4, 7, 4, 6, 1, 1, 9, 9, 6, 6, 5, 3, + 8, 5, 8, 1, 5, 3, 8, 4, 2, 0, 5, 6, 8, 5, 3, 3, 8, 6, 2, 1, 8, 6, 7, 2, 5, 2, 3, 3, 4, 0, 2, 8, 3, 0, 8, 7, 1, 1, 2, 3, 2, 8, 2, 7, 8, 9, 2, 1, 2, 5, 0, 7, 7, 1, 2, 6, 2, 9, 4, 6, 3, 2, 2, 9, 5, 6, 3, 9, 8, 9, + 8, 9, 8, 9, 3, 5, 8, 2, 1, 1, 6, 7, 4, 5, 6, 2, 7, 0, 1, 0, 2, 1, 8, 3, 5, 6, 4, 6, 2, 2, 0, 1, 3, 4, 9, 6, 7, 1, 5, 1, 8, 8, 1, 9, 0, 9, 7, 3, 0, 3, 8, 1, 1, 9, 8, 0, 0, 4, 9, 7, 3, 4, 0, 7, 2, 3, 9, 6, 1, 0, + 3, 6, 8, 5, 4, 0, 6, 6, 4, 3, 1, 9, 3, 9, 5, 0, 9, 7, 9, 0, 1, 9, 0, 6, 9, 9, 6, 3, 9, 5, 5, 2, 4, 5, 3, 0, 0, 5, 4, 5, 0, 5, 8, 0, 6, 8, 5, 5, 0, 1, 9, 5, 6, 7, 3, 0, 2, 2, 9, 2, 1, 9, 1, 3, 9, 3, 3, 9, 1, 8, + 5, 6, 8, 0, 3, 4, 4, 9, 0, 3, 9, 8, 2, 0, 5, 9, 5, 5, 1, 0, 0, 2, 2, 6, 3, 5, 3, 5, 3, 6, 1, 9, 2, 0, 4, 1, 9, 9, 4, 7, 4, 5, 5, 3, 8, 5, 9, 3, 8, 1, 0, 2, 3, 4, 3, 9, 5, 5, 4, 4, 9, 5, 9, 7, 7, 8, 3, 7, 7, 9, + 0, 2, 3, 7, 4, 2, 1, 6, 1, 7, 2, 7, 1, 1, 1, 7, 2, 3, 6, 4, 3, 4, 3, 5, 4, 3, 9, 4, 7, 8, 2, 2, 1, 8, 1, 8, 5, 2, 8, 6, 2, 4, 0, 8, 5, 1, 4, 0, 0, 6, 6, 6, 0, 4, 4, 3, 3, 2, 5, 8, 8, 8, 5, 6, 9, 8, 6, 7, 0, 5, + 4, 3, 1, 5, 4, 7, 0, 6, 9, 6, 5, 7, 4, 7, 4, 5, 8, 5, 5, 0, 3, 3, 2, 3, 2, 3, 3, 4, 2, 1, 0, 7, 3, 0, 1, 5, 4, 5, 9, 4, 0, 5, 1, 6, 5, 5, 3, 7, 9, 0, 6, 8, 6, 6, 2, 7, 3, 3, 3, 7, 9, 9, 5, 8, 5, 1, 1, 5, 6, 2, + 5, 7, 8, 4, 3, 2, 2, 9, 8, 8, 2, 7, 3, 7, 2, 3, 1, 9, 8, 9, 8, 7, 5, 7, 1, 4, 1, 5, 9, 5, 7, 8, 1, 1, 1, 9, 6, 3, 5, 8, 3, 3, 0, 0, 5, 9, 4, 0, 8, 7, 3, 0, 6, 8, 1, 2, 1, 6, 0, 2, 8, 7, 6, 4, 9, 6, 2, 8, 6, 7, + 4, 4, 6, 0, 4, 7, 7, 4, 6, 4, 9, 1, 5, 9, 9, 5, 0, 5, 4, 9, 7, 3, 7, 4, 2, 5, 6, 2, 6, 9, 0, 1, 0, 4, 9, 0, 3, 7, 7, 8, 1, 9, 8, 6, 8, 3, 5, 9, 3, 8, 1, 4, 6, 5, 7, 4, 1, 2, 6, 8, 0, 4, 9, 2, 5, 6, 4, 8, 7, 9, + 8, 5, 5, 6, 1, 4, 5, 3, 7, 2, 3, 4, 7, 8, 6, 7, 3, 3, 0, 3, 9, 0, 4, 6, 8, 8, 3, 8, 3, 4, 3, 6, 3, 4, 6, 5, 5, 3, 7, 9, 4, 9, 8, 6, 4, 1, 9, 2, 7, 0, 5, 6, 3, 8, 7, 2, 9, 3, 1, 7, 4, 8, 7, 2, 3, 3, 2, 0, 8, 3, + 7, 6, 0, 1, 1, 2, 3, 0, 2, 9, 9, 1, 1, 3, 6, 7, 9, 3, 8, 6, 2, 7, 0, 8, 9, 4, 3, 8, 7, 9, 9, 3, 6, 2, 0, 1, 6, 2, 9, 5, 1, 5, 4, 1, 3, 3, 7, 1, 4, 2, 4, 8, 9, 2, 8, 3, 0, 7, 2, 2, 0, 1, 2, 6, 9, 0, 1, 4, 7, 5, + 4, 6, 6, 8, 4, 7, 6, 5, 3, 5, 7, 6, 1, 6, 4, 7, 7, 3, 7, 9, 4, 6, 7, 5, 2, 0, 0, 4, 9, 0, 7, 5, 7, 1, 5, 5, 5, 2, 7, 8, 1, 9, 6, 5, 3, 6, 2, 1, 3, 2, 3, 9, 2, 6, 4, 0, 6, 1, 6, 0, 1, 3, 6, 3, 5, 8, 1, 5, 5, 9, + 0, 7, 4, 2, 2, 0, 2, 0, 2, 0, 3, 1, 8, 7, 2, 7, 7, 6, 0, 5, 2, 7, 7, 2, 1, 9, 0, 0, 5, 5, 6, 1, 4, 8, 4, 2, 5, 5, 5, 1, 8, 7, 9, 2, 5, 3, 0, 3, 4, 3, 5, 1, 3, 9, 8, 4, 4, 2, 5, 3, 2, 2, 3, 4, 1, 5, 7, 6, 2, 3, + 3, 6, 1, 0, 6, 4, 2, 5, 0, 6, 3, 9, 0, 4, 9, 7, 5, 0, 0, 8, 6, 5, 6, 2, 7, 1, 0, 9, 5, 3, 5, 9, 1, 9, 4, 6, 5, 8, 9, 7, 5, 1, 4, 1, 3, 1, 0, 3, 4, 8, 2, 2, 7, 6, 9, 3, 0, 6, 2, 4, 7, 4, 3, 5, 3, 6, 3, 2, 5, 6, + 9, 1, 6, 0, 7, 8, 1, 5, 4, 7, 8, 1, 8, 1, 1, 5, 2, 8, 4, 3, 6, 6, 7, 9, 5, 7, 0, 6, 1, 1, 0, 8, 6, 1, 5, 3, 3, 1, 5, 0, 4, 4, 5, 2, 1, 2, 7, 4, 7, 3, 9, 2, 4, 5, 4, 4, 9, 4, 5, 4, 2, 3, 6, 8, 2, 8, 8, 6, 0, 6, + 1, 3, 4, 0, 8, 4, 1, 4, 8, 6, 3, 7, 7, 6, 7, 0, 0, 9, 6, 1, 2, 0, 7, 1, 5, 1, 2, 4, 9, 1, 4, 0, 4, 3, 0, 2, 7, 2, 5, 3, 8, 6, 0, 7, 6, 4, 8, 2, 3, 6, 3, 4, 1, 4, 3, 3, 4, 6, 2, 3, 5, 1, 8, 9, 7, 5, 7, 6, 6, 4, + 5, 2, 1, 6, 4, 1, 3, 7, 6, 7, 9, 6, 9, 0, 3, 1, 4, 9, 5, 0, 1, 9, 1, 0, 8, 5, 7, 5, 9, 8, 4, 4, 2, 3, 9, 1, 9, 8, 6, 2, 9, 1, 6, 4, 2, 1, 9, 3, 9, 9, 4, 9, 0, 7, 2, 3, 6, 2, 3, 4, 6, 4, 6, 8, 4, 4, 1, 1, 7, 3, + 9, 4, 0, 3, 2, 6, 5, 9, 1, 8, 4, 0, 4, 4, 3, 7, 8, 0, 5, 1, 3, 3, 3, 8, 9, 4, 5, 2, 5, 7, 4, 2, 3, 9, 9, 5, 0, 8, 2, 9, 6, 5, 9, 1, 2, 2, 8, 5, 0, 8, 5, 5, 5, 8, 2, 1, 5, 7, 2, 5, 0, 3, 1, 0, 7, 1, 2, 5, 7, 0, + 1, 2, 6, 6, 8, 3, 0, 2, 4, 0, 2, 9, 2, 9, 5, 2, 5, 2, 2, 0, 1, 1, 8, 7, 2, 6, 7, 6, 7, 5, 6, 2, 2, 0, 4, 1, 5, 4, 2, 0, 5, 1, 6, 1, 8, 4, 1, 6, 3, 4, 8, 4, 7, 5, 6, 5, 1, 6, 9, 9, 9, 8, 1, 1, 6, 1, 4, 1, 0, 1, + 0, 0, 2, 9, 9, 6, 0, 7, 8, 3, 8, 6, 9, 0, 9, 2, 9, 1, 6, 0, 3, 0, 2, 8, 8, 4, 0, 0, 2, 6, 9, 1, 0, 4, 1, 4, 0, 7, 9, 2, 8, 8, 6, 2, 1, 5, 0, 7, 8, 4, 2, 4, 5, 1, 6, 7, 0, 9, 0, 8, 7, 0, 0, 0, 6, 9, 9, 2, 8, 2, + 1, 2, 0, 6, 6, 0, 4, 1, 8, 3, 7, 1, 8, 0, 6, 5, 3, 5, 5, 6, 7, 2, 5, 2, 5, 3, 2, 5, 6, 7, 5, 3, 2, 8, 6, 1, 2, 9, 1, 0, 4, 2, 4, 8, 7, 7, 6, 1, 8, 2, 5, 8, 2, 9, 7, 6, 5, 1, 5, 7, 9, 5, 9, 8, 4, 7, 0, 3, 5, 6, + 2, 2, 2, 6, 2, 9, 3, 4, 8, 6, 0, 0, 3, 4, 1, 5, 8, 7, 2, 2, 9, 8, 0, 5, 3, 4, 9, 8, 9, 6, 5, 0, 2, 2, 6, 2, 9, 1, 7, 4, 8, 7, 8, 8, 2, 0, 2, 7, 3, 4, 2, 0, 9, 2, 2, 2, 2, 4, 5, 3, 3, 9, 8, 5, 6, 2, 6, 4, 7, 6, + 6, 9, 1, 4, 9, 0, 5, 5, 6, 2, 8, 4, 2, 5, 0, 3, 9, 1, 2, 7, 5, 7, 7, 1, 0, 2, 8, 4, 0, 2, 7, 9, 9, 8, 0, 6, 6, 3, 6, 5, 8, 2, 5, 4, 8, 8, 9, 2, 6, 4, 8, 8, 0, 2, 5, 4, 5, 6, 6, 1, 0, 1, 7, 2, 9, 6, 7, 0, 2, 6, + 6, 4, 0, 7, 6, 5, 5, 9, 0, 4, 2, 9, 0, 9, 9, 4, 5, 6, 8, 1, 5, 0, 6, 5, 2, 6, 5, 3, 0, 5, 3, 7, 1, 8, 2, 9, 4, 1, 2, 7, 0, 3, 3, 6, 9, 3, 1, 3, 7, 8, 5, 1, 7, 8, 6, 0, 9, 0, 4, 0, 7, 0, 8, 6, 6, 7, 1, 1, 4, 9, + 6, 5, 5, 8, 3, 4, 3, 4, 3, 4, 7, 6, 9, 3, 3, 8, 5, 7, 8, 1, 7, 1, 1, 3, 8, 6, 4, 5, 5, 8, 7, 3, 6, 7, 8, 1, 2, 3, 0, 1, 4, 5, 8, 7, 6, 8, 7, 1, 2, 6, 6, 0, 3, 4, 8, 9, 1, 3, 9, 0, 9, 5, 6, 2, 0, 0, 9, 9, 3, 9, + 3, 6, 1, 0, 3, 1, 0, 2, 9, 1, 6, 1, 6, 1, 5, 2, 8, 8, 1, 3, 8, 4, 3, 7, 9, 0, 9, 9, 0, 4, 2, 3, 1, 7, 4, 7, 3, 3, 6, 3, 9, 4, 8, 0, 4, 5, 7, 5, 9, 3, 1, 4, 9, 3, 1, 4, 0, 5, 2, 9, 7, 6, 3, 4, 7, 5, 7, 4, 8, 1, + 1, 9, 3, 5, 6, 7, 0, 9, 1, 1, 0, 1, 3, 7, 7, 5, 1, 7, 2, 1, 0, 0, 8, 0, 3, 1, 5, 5, 9, 0, 2, 4, 8, 5, 3, 0, 9, 0, 6, 6, 9, 2, 0, 3, 7, 6, 7, 1, 9, 2, 2, 0, 3, 3, 2, 2, 9, 0, 9, 4, 3, 3, 4, 6, 7, 6, 8, 5, 1, 4, + 2, 2, 1, 4, 4, 7, 7, 3, 7, 9, 3, 9, 3, 7, 5, 1, 7, 0, 3, 4, 4, 3, 6, 6, 1, 9, 9, 1, 0, 4, 0, 3, 3, 7, 5, 1, 1, 1, 7, 3, 5, 4, 7, 1, 9, 1, 8, 5, 5, 0, 4, 6, 4, 4, 9, 0, 2, 6, 3, 6, 5, 5, 1, 2, 8, 1, 6, 2, 2, 8, + 8, 2, 4, 4, 6, 2, 5, 7, 5, 9, 1, 6, 3, 3, 3, 0, 3, 9, 1, 0, 7, 2, 2, 5, 3, 8, 3, 7, 4, 2, 1, 8, 2, 1, 4, 0, 8, 8, 3, 5, 0, 8, 6, 5, 7, 3, 9, 1, 7, 7, 1, 5, 0, 9, 6, 8, 2, 8, 8, 7, 4, 7, 8, 2, 6, 5, 6, 9, 9, 5, + 9, 9, 5, 7, 4, 4, 9, 0, 6, 6, 1, 7, 5, 8, 3, 4, 4, 1, 3, 7, 5, 2, 2, 3, 9, 7, 0, 9, 6, 8, 3, 4, 0, 8, 0, 0, 5, 3, 5, 5, 9, 8, 4, 9, 1, 7, 5, 4, 1, 7, 3, 8, 1, 8, 8, 3, 9, 9, 9, 4, 4, 6, 9, 7, 4, 8, 6, 7, 6, 2, + 6, 5, 5, 1, 6, 5, 8, 2, 7, 6, 5, 8, 4, 8, 3, 5, 8, 8, 4, 5, 3, 1, 4, 2, 7, 7, 5, 6, 8, 7, 9, 0, 0, 2, 9, 0, 9, 5, 1, 7, 0, 2, 8, 3, 5, 2, 9, 7, 1, 6, 3, 4, 4, 5, 6, 2, 1, 2, 9, 6, 4, 0, 4, 3, 5, 2, 3, 1, 1, 7, + 6, 0, 0, 6, 6, 5, 1, 0, 1, 2, 4, 1, 2, 0, 0, 6, 5, 9, 7, 5, 5, 8, 5, 1, 2, 7, 6, 1, 7, 8, 5, 8, 3, 8, 2, 9, 2, 0, 4, 1, 9, 7, 4, 8, 4, 4, 2, 3, 6, 0, 8, 0, 0, 7, 1, 9, 3, 0, 4, 5, 7, 6, 1, 8, 9, 3, 2, 3, 4, 9, + 2, 2, 9, 2, 7, 9, 6, 5, 0, 1, 9, 8, 7, 5, 1, 8, 7, 2, 1, 2, 7, 2, 6, 7, 5, 0, 7, 9, 8, 1, 2, 5, 5, 4, 7, 0, 9, 5, 8, 9, 0, 4, 5, 5, 6, 3, 5, 7, 9, 2, 1, 2, 2, 1, 0, 3, 3, 3, 4, 6, 6, 9, 7, 4, 9, 9, 2, 3, 5, 6, + 3, 0, 2, 5, 4, 9, 4, 7, 8, 0, 2, 4, 9, 0, 1, 1, 4, 1, 9, 5, 2, 1, 2, 3, 8, 2, 8, 1, 5, 3, 0, 9, 1, 1, 4, 0, 7, 9, 0, 7, 3, 8, 6, 0, 2, 5, 1, 5, 2, 2, 7, 4, 2, 9, 9, 5, 8, 1, 8, 0, 7, 2, 4, 7, 1, 6, 2, 5, 9, 1, + 6, 6, 8, 5, 4, 5, 1, 3, 3, 3, 1, 2, 3, 9, 4, 8, 0, 4, 9, 4, 7, 0, 7, 9, 1, 1, 9, 1, 5, 3, 2, 6, 7, 3, 4, 3, 0, 2, 8, 2, 4, 4, 1, 8, 6, 0, 4, 1, 4, 2, 6, 3, 6, 3, 9, 5, 4, 8, 0, 0, 0, 4, 4, 8, 0, 0, 2, 6, 7, 0, + 4, 9, 6, 2, 4, 8, 2, 0, 1, 7, 9, 2, 8, 9, 6, 4, 7, 6, 6, 9, 7, 5, 8, 3, 1, 8, 3, 2, 7, 1, 3, 1, 4, 2, 5, 1, 7, 0, 2, 9, 6, 9, 2, 3, 4, 8, 8, 9, 6, 2, 7, 6, 6, 8, 4, 4, 0, 3, 2, 3, 2, 6, 0, 9, 2, 7, 5, 2, 4, 9, + 6, 0, 3, 5, 7, 9, 9, 6, 4, 6, 9, 2, 5, 6, 5, 0, 4, 9, 3, 6, 8, 1, 8, 3, 6, 0, 9, 0, 0, 3, 2, 3, 8, 0, 9, 2, 9, 3, 4, 5, + 9, 5, 8, 8, 9, 7, 0, 6, 9, 5, 3, 6, 5, 3, 4, 9, 4, 0, 6, 0, 3, 4, 0, 2, 1, 6, 6, 5, 4, 4, 3, 7, 5, 5, 8, 9, 0, 0, 4, 5, 6, 3, 2, 8, 8, 2, 2, 5, 0, 5, 4, 5, 2, 5, 5, 6, 4, 0, 5, 6, 4, 4, 8, 2, 4, 6, 5, 1, 5, 1, + 8, 7, 5, 4, 7, 1, 1, 9, 6, 2, 1, 8, 4, 4, 3, 9, 6, 5, 8, 2, 5, 3, 3, 7, 5, 4, 3, 8, 8, 5, 6, 9, 0, 9, 4, 1, 1, 3, 0, 3, 1, 5, 0, 9, 5, 2, 6, 1, 7, 9, 3, 7, 8, 0, 0, 2, 9, 7, 4, 1, 2, 0, 7, 6, 6, 5, 1, 4, 7, 9, + 3, 9, 4, 2, 5, 9, 0, 2, 9, 8, 9, 6, 9, 5, 9, 4, 6, 9, 9, 5, 5, 6, 5, 7, 6, 1, 2, 1, 8, 6, 5, 6, 1, 9, 6, 7, 3, 3, 7, 8, 6, 2, 3, 6, 2, 5, 6, 1, 2, 5, 2, 1, 6, 3, 2, 0, 8, 6, 2, 8, 6, 9, 2, 2, 2, 1, 0, 3, 2, 7, + 4, 8, 8, 9, 2, 1, 8, 6, 5, 4, 3, 6, 4, 8, 0, 2, 2, 9, 6, 7, 8, 0, 7, 0, 5, 7, 6, 5, 6, 1, 5, 1, 4, 4, 6, 3, 2, 0, 4, 6, 9, 2, 7, 9, 0, 6, 8, 2, 1, 2, 0, 7, 3, 8, 8, 3, 7, 7, 8, 1, 4, 2, 3, 3, 5, 6, 2, 8, 2, 3, + 6, 0, 8, 9, 6, 3, 2, 0, 8, 0, 6, 8, 2, 2, 2, 4, 6, 8, 0, 1, 2, 2, 4, 8, 2, 6, 1, 1, 7, 7, 1, 8, 5, 8, 9, 6, 3, 8, 1, 4, 0, 9, 1, 8, 3, 9, 0, 3, 6, 7, 3, 6, 7, 2, 2, 2, 0, 8, 8, 8, 3, 2, 1, 5, 1, 3, 7, 5, 5, 6, + 0, 0, 3, 7, 2, 7, 9, 8, 3, 9, 4, 0, 0, 4, 1, 5, 2, 9, 7, 0, 0, 2, 8, 7, 8, 3, 0, 7, 6, 6, 7, 0, 9, 4, 4, 4, 7, 4, 5, 6, 0, 1, 3, 4, 5, 5, 6, 4, 1, 7, 2, 5, 4, 3, 7, 0, 9, 0, 6, 9, 7, 9, 3, 9, 6, 1, 2, 2, 5, 7, + 1, 4, 2, 9, 8, 9, 4, 6, 7, 1, 5, 4, 3, 5, 7, 8, 4, 6, 8, 7, 8, 8, 6, 1, 4, 4, 4, 5, 8, 1, 2, 3, 1, 4, 5, 9, 3, 5, 7, 1, 9, 8, 4, 9, 2, 2, 5, 2, 8, 4, 7, 1, 6, 0, 5, 0, 4, 9, 2, 2, 1, 2, 4, 2, 4, 7, 0, 1, 4, 1, + 2, 1, 4, 7, 8, 0, 5, 7, 3, 4, 5, 5, 1, 0, 5, 0, 0, 8, 0, 1, 9, 0, 8, 6, 9, 9, 6, 0, 3, 3, 0, 2, 7, 6, 3, 4, 7, 8, 7, 0, 8, 1, 0, 8, 1, 7, 5, 4, 5, 0, 1, 1, 9, 3, 0, 7, 1, 4, 1, 2, 2, 3, 3, 9, 0, 8, 6, 6, 3, 9, + 3, 8, 3, 3, 9, 5, 2, 9, 4, 2, 5, 7, 8, 6, 9, 0, 5, 0, 7, 6, 4, 3, 1, 0, 0, 6, 3, 8, 3, 5, 1, 9, 8, 3, 4, 3, 8, 9, 3, 4, 1, 5, 9, 6, 1, 3, 1, 8, 5, 4, 3, 4, 7, 5, 4, 6, 4, 9, 5, 5, 6, 9, 7, 8, 1, 0, 3, 8, 2, 9, + 3, 0, 9, 7, 1, 6, 4, 6, 5, 1, 4, 3, 8, 4, 0, 7, 0, 0, 7, 0, 7, 3, 6, 0, 4, 1, 1, 2, 3, 7, 3, 5, 9, 9, 8, 4, 3, 4, 5, 2, 2, 5, 1, 6, 1, 0, 5, 0, 7, 0, 2, 7, 0, 5, 6, 2, 3, 5, 2, 6, 6, 0, 1, 2, 7, 6, 4, 8, 4, 8, + 3, 0, 8, 4, 0, 7, 6, 1, 1, 8, 3, 0, 1, 3, 0, 5, 2, 7, 9, 3, 2, 0, 5, 4, 2, 7, 4, 6, 2, 8, 6, 5, 4, 0, 3, 6, 0, 3, 6, 7, 4, 5, 3, 2, 8, 6, 5, 1, 0, 5, 7, 0, 6, 5, 8, 7, 4, 8, 8, 2, 2, 5, 6, 9, 8, 1, 5, 7, 9, 3, + 6, 7, 8, 9, 7, 6, 6, 9, 7, 4, 2, 2, 0, 5, 7, 5, 0, 5, 9, 6, 8, 3, 4, 4, 0, 8, 6, 9, 7, 3, 5, 0, 2, 0, 1, 4, 1, 0, 2, 0, 6, 7, 2, 3, 5, 8, 5, 0, 2, 0, 0, 7, 2, 4, 5, 2, 2, 5, 6, 3, 2, 6, 5, 1, 3, 4, 1, 0, 5, 5, + 9, 2, 4, 0, 1, 9, 0, 2, 7, 4, 2, 1, 6, 2, 4, 8, 4, 3, 9, 1, 4, 0, 3, 5, 9, 9, 8, 9, 5, 3, 5, 3, 9, 4, 5, 9, 0, 9, 4, 4, 0, 7, 0, 4, 6, 9, 1, 2, 0, 9, 1, 4, 0, 9, 3, 8, 7, 0, 0, 1, 2, 6, 4, 5, 6, 0, 0, 1, 6, 2, + 3, 7, 4, 2, 8, 8, 0, 2, 1, 0, 9, 2, 7, 6, 4, 5, 7, 9, 3, 1, 0, 6, 5, 7, 9, 2, 2, 9, 5, 5, 2, 4, 9, 8, 8, 7, 2, 7, 5, 8, 4, 6, 1, 0, 1, 2, 6, 4, 8, 3, 6, 9, 9, 9, 8, 9, 2, 2, 5, 6, 9, 5, 9, 6, 8, 8, 1, 5, 9, 2, + 0, 5, 6, 0, 0, 1, 0, 1, 6, 5, 5, 2, 5, 6, 3, 7, 5, 6, 7, 8 + }; +} diff --git a/src/main/java/android/os/Message.java b/src/main/java/android/os/Message.java new file mode 100644 index 0000000..8c75847 --- /dev/null +++ b/src/main/java/android/os/Message.java @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.util.TimeUtils; + +/** + * + * Defines a message containing a description and arbitrary data object that can be + * sent to a {@link Handler}. This object contains two extra int fields and an + * extra object field that allow you to not do allocations in many cases. + * + *

While the constructor of Message is public, the best way to get + * one of these is to call {@link #obtain Message.obtain()} or one of the + * {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull + * them from a pool of recycled objects.

+ */ +public final class Message implements Parcelable { + /** + * User-defined message code so that the recipient can identify + * what this message is about. Each {@link Handler} has its own name-space + * for message codes, so you do not need to worry about yours conflicting + * with other handlers. + */ + public int what; + + /** + * arg1 and arg2 are lower-cost alternatives to using + * {@link #setData(Bundle) setData()} if you only need to store a + * few integer values. + */ + public int arg1; + + /** + * arg1 and arg2 are lower-cost alternatives to using + * {@link #setData(Bundle) setData()} if you only need to store a + * few integer values. + */ + public int arg2; + + /** + * An arbitrary object to send to the recipient. When using + * {@link Messenger} to send the message across processes this can only + * be non-null if it contains a Parcelable of a framework class (not one + * implemented by the application). For other data transfer use + * {@link #setData}. + * + *

Note that Parcelable objects here are not supported prior to + * the {@link android.os.Build.VERSION_CODES#FROYO} release. + */ + public Object obj; + + /** + * Optional Messenger where replies to this message can be sent. The + * semantics of exactly how this is used are up to the sender and + * receiver. + */ + public Messenger replyTo; + + /** + * Optional field indicating the uid that sent the message. This is + * only valid for messages posted by a {@link Messenger}; otherwise, + * it will be -1. + */ + public int sendingUid = -1; + + /** If set message is in use. + * This flag is set when the message is enqueued and remains set while it + * is delivered and afterwards when it is recycled. The flag is only cleared + * when a new message is created or obtained since that is the only time that + * applications are allowed to modify the contents of the message. + * + * It is an error to attempt to enqueue or recycle a message that is already in use. + */ + /*package*/ static final int FLAG_IN_USE = 1 << 0; + + /** If set message is asynchronous */ + /*package*/ static final int FLAG_ASYNCHRONOUS = 1 << 1; + + /** Flags to clear in the copyFrom method */ + /*package*/ static final int FLAGS_TO_CLEAR_ON_COPY_FROM = FLAG_IN_USE; + + /*package*/ int flags; + + /*package*/ long when; + + /*package*/ Bundle data; + + /*package*/ Handler target; + + /*package*/ Runnable callback; + + // sometimes we store linked lists of these things + /*package*/ Message next; + + private static final Object sPoolSync = new Object(); + private static Message sPool; + private static int sPoolSize = 0; + + private static final int MAX_POOL_SIZE = 50; + + private static boolean gCheckRecycle = true; + + /** + * Return a new Message instance from the global pool. Allows us to + * avoid allocating new objects in many cases. + */ + public static Message obtain() { + synchronized (sPoolSync) { + if (sPool != null) { + Message m = sPool; + sPool = m.next; + m.next = null; + m.flags = 0; // clear in-use flag + sPoolSize--; + return m; + } + } + return new Message(); + } + + /** + * Same as {@link #obtain()}, but copies the values of an existing + * message (including its target) into the new one. + * @param orig Original message to copy. + * @return A Message object from the global pool. + */ + public static Message obtain(Message orig) { + Message m = obtain(); + m.what = orig.what; + m.arg1 = orig.arg1; + m.arg2 = orig.arg2; + m.obj = orig.obj; + m.replyTo = orig.replyTo; + m.sendingUid = orig.sendingUid; + if (orig.data != null) { + m.data = new Bundle(orig.data); + } + m.target = orig.target; + m.callback = orig.callback; + + return m; + } + + /** + * Same as {@link #obtain()}, but sets the value for the target member on the Message returned. + * @param h Handler to assign to the returned Message object's target member. + * @return A Message object from the global pool. + */ + public static Message obtain(Handler h) { + Message m = obtain(); + m.target = h; + + return m; + } + + /** + * Same as {@link #obtain(Handler)}, but assigns a callback Runnable on + * the Message that is returned. + * @param h Handler to assign to the returned Message object's target member. + * @param callback Runnable that will execute when the message is handled. + * @return A Message object from the global pool. + */ + public static Message obtain(Handler h, Runnable callback) { + Message m = obtain(); + m.target = h; + m.callback = callback; + + return m; + } + + /** + * Same as {@link #obtain()}, but sets the values for both target and + * what members on the Message. + * @param h Value to assign to the target member. + * @param what Value to assign to the what member. + * @return A Message object from the global pool. + */ + public static Message obtain(Handler h, int what) { + Message m = obtain(); + m.target = h; + m.what = what; + + return m; + } + + /** + * Same as {@link #obtain()}, but sets the values of the target, what, and obj + * members. + * @param h The target value to set. + * @param what The what value to set. + * @param obj The object method to set. + * @return A Message object from the global pool. + */ + public static Message obtain(Handler h, int what, Object obj) { + Message m = obtain(); + m.target = h; + m.what = what; + m.obj = obj; + + return m; + } + + /** + * Same as {@link #obtain()}, but sets the values of the target, what, + * arg1, and arg2 members. + * + * @param h The target value to set. + * @param what The what value to set. + * @param arg1 The arg1 value to set. + * @param arg2 The arg2 value to set. + * @return A Message object from the global pool. + */ + public static Message obtain(Handler h, int what, int arg1, int arg2) { + Message m = obtain(); + m.target = h; + m.what = what; + m.arg1 = arg1; + m.arg2 = arg2; + + return m; + } + + /** + * Same as {@link #obtain()}, but sets the values of the target, what, + * arg1, arg2, and obj members. + * + * @param h The target value to set. + * @param what The what value to set. + * @param arg1 The arg1 value to set. + * @param arg2 The arg2 value to set. + * @param obj The obj value to set. + * @return A Message object from the global pool. + */ + public static Message obtain(Handler h, int what, + int arg1, int arg2, Object obj) { + Message m = obtain(); + m.target = h; + m.what = what; + m.arg1 = arg1; + m.arg2 = arg2; + m.obj = obj; + + return m; + } + + /** @hide */ + public static void updateCheckRecycle(int targetSdkVersion) { + if (targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) { + gCheckRecycle = false; + } + } + + /** + * Return a Message instance to the global pool. + *

+ * You MUST NOT touch the Message after calling this function because it has + * effectively been freed. It is an error to recycle a message that is currently + * enqueued or that is in the process of being delivered to a Handler. + *

+ */ + public void recycle() { + if (isInUse()) { + if (gCheckRecycle) { + throw new IllegalStateException("This message cannot be recycled because it " + + "is still in use."); + } + return; + } + recycleUnchecked(); + } + + /** + * Recycles a Message that may be in-use. + * Used internally by the MessageQueue and Looper when disposing of queued Messages. + */ + void recycleUnchecked() { + // Mark the message as in use while it remains in the recycled object pool. + // Clear out all other details. + flags = FLAG_IN_USE; + what = 0; + arg1 = 0; + arg2 = 0; + obj = null; + replyTo = null; + sendingUid = -1; + when = 0; + target = null; + callback = null; + data = null; + + synchronized (sPoolSync) { + if (sPoolSize < MAX_POOL_SIZE) { + next = sPool; + sPool = this; + sPoolSize++; + } + } + } + + /** + * Make this message like o. Performs a shallow copy of the data field. + * Does not copy the linked list fields, nor the timestamp or + * target/callback of the original message. + */ + public void copyFrom(Message o) { + this.flags = o.flags & ~FLAGS_TO_CLEAR_ON_COPY_FROM; + this.what = o.what; + this.arg1 = o.arg1; + this.arg2 = o.arg2; + this.obj = o.obj; + this.replyTo = o.replyTo; + this.sendingUid = o.sendingUid; + + if (o.data != null) { + this.data = (Bundle) o.data.clone(); + } else { + this.data = null; + } + } + + /** + * Return the targeted delivery time of this message, in milliseconds. + */ + public long getWhen() { + return when; + } + + public void setTarget(Handler target) { + this.target = target; + } + + /** + * Retrieve the a {@link android.os.Handler Handler} implementation that + * will receive this message. The object must implement + * {@link android.os.Handler#handleMessage(android.os.Message) + * Handler.handleMessage()}. Each Handler has its own name-space for + * message codes, so you do not need to + * worry about yours conflicting with other handlers. + */ + public Handler getTarget() { + return target; + } + + /** + * Retrieve callback object that will execute when this message is handled. + * This object must implement Runnable. This is called by + * the target {@link Handler} that is receiving this Message to + * dispatch it. If + * not set, the message will be dispatched to the receiving Handler's + * {@link Handler#handleMessage(Message Handler.handleMessage())}. + */ + public Runnable getCallback() { + return callback; + } + + /** + * Obtains a Bundle of arbitrary data associated with this + * event, lazily creating it if necessary. Set this value by calling + * {@link #setData(Bundle)}. Note that when transferring data across + * processes via {@link Messenger}, you will need to set your ClassLoader + * on the Bundle via {@link Bundle#setClassLoader(ClassLoader) + * Bundle.setClassLoader()} so that it can instantiate your objects when + * you retrieve them. + * @see #peekData() + * @see #setData(Bundle) + */ + public Bundle getData() { + if (data == null) { + data = new Bundle(); + } + + return data; + } + + /** + * Like getData(), but does not lazily create the Bundle. A null + * is returned if the Bundle does not already exist. See + * {@link #getData} for further information on this. + * @see #getData() + * @see #setData(Bundle) + */ + public Bundle peekData() { + return data; + } + + /** + * Sets a Bundle of arbitrary data values. Use arg1 and arg2 members + * as a lower cost way to send a few simple integer values, if you can. + * @see #getData() + * @see #peekData() + */ + public void setData(Bundle data) { + this.data = data; + } + + /** + * Sends this Message to the Handler specified by {@link #getTarget}. + * Throws a null pointer exception if this field has not been set. + */ + public void sendToTarget() { + target.sendMessage(this); + } + + /** + * Returns true if the message is asynchronous, meaning that it is not + * subject to {@link Looper} synchronization barriers. + * + * @return True if the message is asynchronous. + * + * @see #setAsynchronous(boolean) + */ + public boolean isAsynchronous() { + return (flags & FLAG_ASYNCHRONOUS) != 0; + } + + /** + * Sets whether the message is asynchronous, meaning that it is not + * subject to {@link Looper} synchronization barriers. + *

+ * Certain operations, such as view invalidation, may introduce synchronization + * barriers into the {@link Looper}'s message queue to prevent subsequent messages + * from being delivered until some condition is met. In the case of view invalidation, + * messages which are posted after a call to {@link android.view.View#invalidate} + * are suspended by means of a synchronization barrier until the next frame is + * ready to be drawn. The synchronization barrier ensures that the invalidation + * request is completely handled before resuming. + *

+ * Asynchronous messages are exempt from synchronization barriers. They typically + * represent interrupts, input events, and other signals that must be handled independently + * even while other work has been suspended. + *

+ * Note that asynchronous messages may be delivered out of order with respect to + * synchronous messages although they are always delivered in order among themselves. + * If the relative order of these messages matters then they probably should not be + * asynchronous in the first place. Use with caution. + *

+ * + * @param async True if the message is asynchronous. + * + * @see #isAsynchronous() + */ + public void setAsynchronous(boolean async) { + if (async) { + flags |= FLAG_ASYNCHRONOUS; + } else { + flags &= ~FLAG_ASYNCHRONOUS; + } + } + + /*package*/ boolean isInUse() { + return ((flags & FLAG_IN_USE) == FLAG_IN_USE); + } + + /*package*/ void markInUse() { + flags |= FLAG_IN_USE; + } + + /** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}). + */ + public Message() { + } + + @Override + public String toString() { + return toString(SystemClock.uptimeMillis()); + } + + String toString(long now) { + StringBuilder b = new StringBuilder(); + b.append("{ when="); + TimeUtils.formatDuration(when - now, b); + + if (target != null) { + if (callback != null) { + b.append(" callback="); + b.append(callback.getClass().getName()); + } else { + b.append(" what="); + b.append(what); + } + + if (arg1 != 0) { + b.append(" arg1="); + b.append(arg1); + } + + if (arg2 != 0) { + b.append(" arg2="); + b.append(arg2); + } + + if (obj != null) { + b.append(" obj="); + b.append(obj); + } + + b.append(" target="); + b.append(target.getClass().getName()); + } else { + b.append(" barrier="); + b.append(arg1); + } + + b.append(" }"); + return b.toString(); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public Message createFromParcel(Parcel source) { + Message msg = Message.obtain(); + msg.readFromParcel(source); + return msg; + } + + public Message[] newArray(int size) { + return new Message[size]; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + if (callback != null) { + throw new RuntimeException( + "Can't marshal callbacks across processes."); + } + dest.writeInt(what); + dest.writeInt(arg1); + dest.writeInt(arg2); + if (obj != null) { + try { + Parcelable p = (Parcelable)obj; + dest.writeInt(1); + dest.writeParcelable(p, flags); + } catch (ClassCastException e) { + throw new RuntimeException( + "Can't marshal non-Parcelable objects across processes."); + } + } else { + dest.writeInt(0); + } + dest.writeLong(when); + dest.writeBundle(data); + Messenger.writeMessengerOrNullToParcel(replyTo, dest); + dest.writeInt(sendingUid); + } + + private void readFromParcel(Parcel source) { + what = source.readInt(); + arg1 = source.readInt(); + arg2 = source.readInt(); + if (source.readInt() != 0) { + obj = source.readParcelable(getClass().getClassLoader()); + } + when = source.readLong(); + data = source.readBundle(); + replyTo = Messenger.readMessengerOrNullFromParcel(source); + sendingUid = source.readInt(); + } +} diff --git a/src/main/java/android/os/MessageQueue.java b/src/main/java/android/os/MessageQueue.java new file mode 100644 index 0000000..01a23ce --- /dev/null +++ b/src/main/java/android/os/MessageQueue.java @@ -0,0 +1,566 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.util.Log; +import android.util.Printer; + +import java.util.ArrayList; + +/** + * Low-level class holding the list of messages to be dispatched by a + * {@link Looper}. Messages are not added directly to a MessageQueue, + * but rather through {@link Handler} objects associated with the Looper. + * + *

You can retrieve the MessageQueue for the current thread with + * {@link Looper#myQueue() Looper.myQueue()}. + */ +public final class MessageQueue { + // True if the message queue can be quit. + private final boolean mQuitAllowed; + + @SuppressWarnings("unused") + private long mPtr; // used by native code + + Message mMessages; + private final ArrayList mIdleHandlers = new ArrayList(); + private IdleHandler[] mPendingIdleHandlers; + private boolean mQuitting; + + // Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout. + private boolean mBlocked; + + // The next barrier token. + // Barriers are indicated by messages with a null target whose arg1 field carries the token. + private int mNextBarrierToken; + + private native static long nativeInit(); + private native static void nativeDestroy(long ptr); + private native static void nativePollOnce(long ptr, int timeoutMillis); + private native static void nativeWake(long ptr); + private native static boolean nativeIsIdling(long ptr); + + /** + * Callback interface for discovering when a thread is going to block + * waiting for more messages. + */ + public static interface IdleHandler { + /** + * Called when the message queue has run out of messages and will now + * wait for more. Return true to keep your idle handler active, false + * to have it removed. This may be called if there are still messages + * pending in the queue, but they are all scheduled to be dispatched + * after the current time. + */ + boolean queueIdle(); + } + + /** + * Add a new {@link IdleHandler} to this message queue. This may be + * removed automatically for you by returning false from + * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is + * invoked, or explicitly removing it with {@link #removeIdleHandler}. + * + *

This method is safe to call from any thread. + * + * @param handler The IdleHandler to be added. + */ + public void addIdleHandler(IdleHandler handler) { + if (handler == null) { + throw new NullPointerException("Can't add a null IdleHandler"); + } + synchronized (this) { + mIdleHandlers.add(handler); + } + } + + /** + * Remove an {@link IdleHandler} from the queue that was previously added + * with {@link #addIdleHandler}. If the given object is not currently + * in the idle list, nothing is done. + * + * @param handler The IdleHandler to be removed. + */ + public void removeIdleHandler(IdleHandler handler) { + synchronized (this) { + mIdleHandlers.remove(handler); + } + } + + MessageQueue(boolean quitAllowed) { + mQuitAllowed = quitAllowed; + mPtr = nativeInit(); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(); + } finally { + super.finalize(); + } + } + + // Disposes of the underlying message queue. + // Must only be called on the looper thread or the finalizer. + private void dispose() { + if (mPtr != 0) { + nativeDestroy(mPtr); + mPtr = 0; + } + } + + Message next() { + // Return here if the message loop has already quit and been disposed. + // This can happen if the application tries to restart a looper after quit + // which is not supported. + final long ptr = mPtr; + if (ptr == 0) { + return null; + } + + int pendingIdleHandlerCount = -1; // -1 only during first iteration + int nextPollTimeoutMillis = 0; + for (;;) { + if (nextPollTimeoutMillis != 0) { + Binder.flushPendingCommands(); + } + + nativePollOnce(ptr, nextPollTimeoutMillis); + + synchronized (this) { + // Try to retrieve the next message. Return if found. + final long now = SystemClock.uptimeMillis(); + Message prevMsg = null; + Message msg = mMessages; + if (msg != null && msg.target == null) { + // Stalled by a barrier. Find the next asynchronous message in the queue. + do { + prevMsg = msg; + msg = msg.next; + } while (msg != null && !msg.isAsynchronous()); + } + if (msg != null) { + if (now < msg.when) { + // Next message is not ready. Set a timeout to wake up when it is ready. + nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); + } else { + // Got a message. + mBlocked = false; + if (prevMsg != null) { + prevMsg.next = msg.next; + } else { + mMessages = msg.next; + } + msg.next = null; + if (false) Log.v("MessageQueue", "Returning message: " + msg); + return msg; + } + } else { + // No more messages. + nextPollTimeoutMillis = -1; + } + + // Process the quit message now that all pending messages have been handled. + if (mQuitting) { + dispose(); + return null; + } + + // If first time idle, then get the number of idlers to run. + // Idle handles only run if the queue is empty or if the first message + // in the queue (possibly a barrier) is due to be handled in the future. + if (pendingIdleHandlerCount < 0 + && (mMessages == null || now < mMessages.when)) { + pendingIdleHandlerCount = mIdleHandlers.size(); + } + if (pendingIdleHandlerCount <= 0) { + // No idle handlers to run. Loop and wait some more. + mBlocked = true; + continue; + } + + if (mPendingIdleHandlers == null) { + mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; + } + mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); + } + + // Run the idle handlers. + // We only ever reach this code block during the first iteration. + for (int i = 0; i < pendingIdleHandlerCount; i++) { + final IdleHandler idler = mPendingIdleHandlers[i]; + mPendingIdleHandlers[i] = null; // release the reference to the handler + + boolean keep = false; + try { + keep = idler.queueIdle(); + } catch (Throwable t) { + Log.wtf("MessageQueue", "IdleHandler threw exception", t); + } + + if (!keep) { + synchronized (this) { + mIdleHandlers.remove(idler); + } + } + } + + // Reset the idle handler count to 0 so we do not run them again. + pendingIdleHandlerCount = 0; + + // While calling an idle handler, a new message could have been delivered + // so go back and look again for a pending message without waiting. + nextPollTimeoutMillis = 0; + } + } + + void quit(boolean safe) { + if (!mQuitAllowed) { + throw new IllegalStateException("Main thread not allowed to quit."); + } + + synchronized (this) { + if (mQuitting) { + return; + } + mQuitting = true; + + if (safe) { + removeAllFutureMessagesLocked(); + } else { + removeAllMessagesLocked(); + } + + // We can assume mPtr != 0 because mQuitting was previously false. + nativeWake(mPtr); + } + } + + int enqueueSyncBarrier(long when) { + // Enqueue a new sync barrier token. + // We don't need to wake the queue because the purpose of a barrier is to stall it. + synchronized (this) { + final int token = mNextBarrierToken++; + final Message msg = Message.obtain(); + msg.markInUse(); + msg.when = when; + msg.arg1 = token; + + Message prev = null; + Message p = mMessages; + if (when != 0) { + while (p != null && p.when <= when) { + prev = p; + p = p.next; + } + } + if (prev != null) { // invariant: p == prev.next + msg.next = p; + prev.next = msg; + } else { + msg.next = p; + mMessages = msg; + } + return token; + } + } + + void removeSyncBarrier(int token) { + // Remove a sync barrier token from the queue. + // If the queue is no longer stalled by a barrier then wake it. + synchronized (this) { + Message prev = null; + Message p = mMessages; + while (p != null && (p.target != null || p.arg1 != token)) { + prev = p; + p = p.next; + } + if (p == null) { + throw new IllegalStateException("The specified message queue synchronization " + + " barrier token has not been posted or has already been removed."); + } + final boolean needWake; + if (prev != null) { + prev.next = p.next; + needWake = false; + } else { + mMessages = p.next; + needWake = mMessages == null || mMessages.target != null; + } + p.recycleUnchecked(); + + // If the loop is quitting then it is already awake. + // We can assume mPtr != 0 when mQuitting is false. + if (needWake && !mQuitting) { + nativeWake(mPtr); + } + } + } + + boolean enqueueMessage(Message msg, long when) { + if (msg.target == null) { + throw new IllegalArgumentException("Message must have a target."); + } + if (msg.isInUse()) { + throw new IllegalStateException(msg + " This message is already in use."); + } + + synchronized (this) { + if (mQuitting) { + IllegalStateException e = new IllegalStateException( + msg.target + " sending message to a Handler on a dead thread"); + Log.w("MessageQueue", e.getMessage(), e); + msg.recycle(); + return false; + } + + msg.markInUse(); + msg.when = when; + Message p = mMessages; + boolean needWake; + if (p == null || when == 0 || when < p.when) { + // New head, wake up the event queue if blocked. + msg.next = p; + mMessages = msg; + needWake = mBlocked; + } else { + // Inserted within the middle of the queue. Usually we don't have to wake + // up the event queue unless there is a barrier at the head of the queue + // and the message is the earliest asynchronous message in the queue. + needWake = mBlocked && p.target == null && msg.isAsynchronous(); + Message prev; + for (;;) { + prev = p; + p = p.next; + if (p == null || when < p.when) { + break; + } + if (needWake && p.isAsynchronous()) { + needWake = false; + } + } + msg.next = p; // invariant: p == prev.next + prev.next = msg; + } + + // We can assume mPtr != 0 because mQuitting is false. + if (needWake) { + nativeWake(mPtr); + } + } + return true; + } + + boolean hasMessages(Handler h, int what, Object object) { + if (h == null) { + return false; + } + + synchronized (this) { + Message p = mMessages; + while (p != null) { + if (p.target == h && p.what == what && (object == null || p.obj == object)) { + return true; + } + p = p.next; + } + return false; + } + } + + boolean hasMessages(Handler h, Runnable r, Object object) { + if (h == null) { + return false; + } + + synchronized (this) { + Message p = mMessages; + while (p != null) { + if (p.target == h && p.callback == r && (object == null || p.obj == object)) { + return true; + } + p = p.next; + } + return false; + } + } + + boolean isIdling() { + synchronized (this) { + return isIdlingLocked(); + } + } + + private boolean isIdlingLocked() { + // If the loop is quitting then it must not be idling. + // We can assume mPtr != 0 when mQuitting is false. + return !mQuitting && nativeIsIdling(mPtr); + } + + void removeMessages(Handler h, int what, Object object) { + if (h == null) { + return; + } + + synchronized (this) { + Message p = mMessages; + + // Remove all messages at front. + while (p != null && p.target == h && p.what == what + && (object == null || p.obj == object)) { + Message n = p.next; + mMessages = n; + p.recycleUnchecked(); + p = n; + } + + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && n.what == what + && (object == null || n.obj == object)) { + Message nn = n.next; + n.recycleUnchecked(); + p.next = nn; + continue; + } + } + p = n; + } + } + } + + void removeMessages(Handler h, Runnable r, Object object) { + if (h == null || r == null) { + return; + } + + synchronized (this) { + Message p = mMessages; + + // Remove all messages at front. + while (p != null && p.target == h && p.callback == r + && (object == null || p.obj == object)) { + Message n = p.next; + mMessages = n; + p.recycleUnchecked(); + p = n; + } + + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && n.callback == r + && (object == null || n.obj == object)) { + Message nn = n.next; + n.recycleUnchecked(); + p.next = nn; + continue; + } + } + p = n; + } + } + } + + void removeCallbacksAndMessages(Handler h, Object object) { + if (h == null) { + return; + } + + synchronized (this) { + Message p = mMessages; + + // Remove all messages at front. + while (p != null && p.target == h + && (object == null || p.obj == object)) { + Message n = p.next; + mMessages = n; + p.recycleUnchecked(); + p = n; + } + + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && (object == null || n.obj == object)) { + Message nn = n.next; + n.recycleUnchecked(); + p.next = nn; + continue; + } + } + p = n; + } + } + } + + private void removeAllMessagesLocked() { + Message p = mMessages; + while (p != null) { + Message n = p.next; + p.recycleUnchecked(); + p = n; + } + mMessages = null; + } + + private void removeAllFutureMessagesLocked() { + final long now = SystemClock.uptimeMillis(); + Message p = mMessages; + if (p != null) { + if (p.when > now) { + removeAllMessagesLocked(); + } else { + Message n; + for (;;) { + n = p.next; + if (n == null) { + return; + } + if (n.when > now) { + break; + } + p = n; + } + p.next = null; + do { + p = n; + n = p.next; + p.recycleUnchecked(); + } while (n != null); + } + } + } + + void dump(Printer pw, String prefix) { + synchronized (this) { + long now = SystemClock.uptimeMillis(); + int n = 0; + for (Message msg = mMessages; msg != null; msg = msg.next) { + pw.println(prefix + "Message " + n + ": " + msg.toString(now)); + n++; + } + pw.println(prefix + "(Total messages: " + n + ", idling=" + isIdlingLocked() + + ", quitting=" + mQuitting + ")"); + } + } +} diff --git a/src/main/java/android/os/MessageQueueTest.java b/src/main/java/android/os/MessageQueueTest.java new file mode 100644 index 0000000..f82bfce --- /dev/null +++ b/src/main/java/android/os/MessageQueueTest.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.test.suitebuilder.annotation.MediumTest; +import junit.framework.TestCase; + +public class MessageQueueTest extends TestCase { + + private static class BaseTestHandler extends TestHandlerThread { + Handler mHandler; + int mLastMessage; + int mCount; + + public BaseTestHandler() { + } + + public void go() { + mHandler = new Handler() { + public void handleMessage(Message msg) { + BaseTestHandler.this.handleMessage(msg); + } + }; + } + + public void handleMessage(Message msg) { + if (!msg.isInUse()) { + failure(new RuntimeException( + "msg.isInuse is false, should always be true, #" + msg.what)); + } + if (mCount <= mLastMessage) { + if (msg.what != mCount) { + failure(new RuntimeException( + "Expected message #" + mCount + + ", received #" + msg.what)); + } else if (mCount == mLastMessage) { + success(); + } + mCount++; + } else { + failure(new RuntimeException( + "Message received after done, #" + msg.what)); + } + } + } + + @MediumTest + public void testMessageOrder() throws Exception { + TestHandlerThread tester = new BaseTestHandler() { + public void go() { + super.go(); + long now = SystemClock.uptimeMillis() + 200; + mLastMessage = 4; + mCount = 0; + mHandler.sendMessageAtTime(mHandler.obtainMessage(2), now + 1); + mHandler.sendMessageAtTime(mHandler.obtainMessage(3), now + 2); + mHandler.sendMessageAtTime(mHandler.obtainMessage(4), now + 2); + mHandler.sendMessageAtTime(mHandler.obtainMessage(0), now + 0); + mHandler.sendMessageAtTime(mHandler.obtainMessage(1), now + 0); + } + }; + + tester.doTest(1000); + } + + @MediumTest + public void testAtFrontOfQueue() throws Exception { + TestHandlerThread tester = new BaseTestHandler() { + public void go() { + super.go(); + long now = SystemClock.uptimeMillis() + 200; + mLastMessage = 3; + mCount = 0; + mHandler.sendMessageAtTime(mHandler.obtainMessage(3), now); + mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(2)); + mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(0)); + } + + public void handleMessage(Message msg) { + super.handleMessage(msg); + if (msg.what == 0) { + mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(1)); + } + } + }; + + tester.doTest(1000); + } + + private static class TestFieldIntegrityHandler extends TestHandlerThread { + Handler mHandler; + int mLastMessage; + int mCount; + + public TestFieldIntegrityHandler() { + } + + public void go() { + mHandler = new Handler() { + public void handleMessage(Message msg) { + TestFieldIntegrityHandler.this.handleMessage(msg); + } + }; + } + + public void handleMessage(Message msg) { + if (!msg.isInUse()) { + failure(new RuntimeException( + "msg.isInuse is false, should always be true, #" + msg.what)); + } + if (mCount <= mLastMessage) { + if (msg.what != mCount) { + failure(new RuntimeException( + "Expected message #" + mCount + + ", received #" + msg.what)); + } else if (mCount == mLastMessage) { + success(); + } + mCount++; + } else { + failure(new RuntimeException( + "Message received after done, #" + msg.what)); + } + } + } + + @MediumTest + public void testFieldIntegrity() throws Exception { + + TestHandlerThread tester = new TestFieldIntegrityHandler() { + Bundle mBundle; + + public void go() { + super.go(); + mLastMessage = 1; + mCount = 0; + mHandler.sendMessage(mHandler.obtainMessage(0)); + } + + public void handleMessage(Message msg) { + super.handleMessage(msg); + if (msg.what == 0) { + msg.flags = -1; + msg.what = 1; + msg.arg1 = 456; + msg.arg2 = 789; + msg.obj = this; + msg.replyTo = null; + mBundle = new Bundle(); + msg.data = mBundle; + msg.data.putString("key", "value"); + + Message newMsg = mHandler.obtainMessage(); + newMsg.copyFrom(msg); + if (newMsg.isInUse() != false) { + failure(new RuntimeException( + "newMsg.isInUse is true should be false after copyFrom")); + } + if (newMsg.flags != 0) { + failure(new RuntimeException(String.format( + "newMsg.flags is %d should be 0 after copyFrom", newMsg.flags))); + } + if (newMsg.what != 1) { + failure(new RuntimeException(String.format( + "newMsg.what is %d should be %d after copyFrom", newMsg.what, 1))); + } + if (newMsg.arg1 != 456) { + failure(new RuntimeException(String.format( + "newMsg.arg1 is %d should be %d after copyFrom", msg.arg1, 456))); + } + if (newMsg.arg2 != 789) { + failure(new RuntimeException(String.format( + "newMsg.arg2 is %d should be %d after copyFrom", msg.arg2, 789))); + } + if (newMsg.obj != this) { + failure(new RuntimeException( + "newMsg.obj should be 'this' after copyFrom")); + } + if (newMsg.replyTo != null) { + failure(new RuntimeException( + "newMsg.replyTo should be null after copyFrom")); + } + if (newMsg.data == mBundle) { + failure(new RuntimeException( + "newMsg.data should NOT be mBundle after copyFrom")); + } + if (!newMsg.data.getString("key").equals(mBundle.getString("key"))) { + failure(new RuntimeException(String.format( + "newMsg.data.getString(\"key\") is %s and does not equal" + + " mBundle.getString(\"key\") which is %s after copyFrom", + newMsg.data.getString("key"), mBundle.getString("key")))); + } + if (newMsg.when != 0) { + failure(new RuntimeException(String.format( + "newMsg.when is %d should be 0 after copyFrom", newMsg.when))); + } + if (newMsg.target != mHandler) { + failure(new RuntimeException( + "newMsg.target is NOT mHandler after copyFrom")); + } + if (newMsg.callback != null) { + failure(new RuntimeException( + "newMsg.callback is NOT null after copyFrom")); + } + + mHandler.sendMessage(newMsg); + } else if (msg.what == 1) { + if (msg.isInUse() != true) { + failure(new RuntimeException(String.format( + "msg.isInUse is false should be true after when processing %d", + msg.what))); + } + if (msg.arg1 != 456) { + failure(new RuntimeException(String.format( + "msg.arg1 is %d should be %d when processing # %d", + msg.arg1, 456, msg.what))); + } + if (msg.arg2 != 789) { + failure(new RuntimeException(String.format( + "msg.arg2 is %d should be %d when processing # %d", + msg.arg2, 789, msg.what))); + } + if (msg.obj != this) { + failure(new RuntimeException(String.format( + "msg.obj should be 'this' when processing # %d", msg.what))); + } + if (msg.replyTo != null) { + failure(new RuntimeException(String.format( + "msg.replyTo should be null when processing # %d", msg.what))); + } + if (!msg.data.getString("key").equals(mBundle.getString("key"))) { + failure(new RuntimeException(String.format( + "msg.data.getString(\"key\") is %s and does not equal" + + " mBundle.getString(\"key\") which is %s when processing # %d", + msg.data.getString("key"), mBundle.getString("key"), msg.what))); + } + if (msg.when != 0) { + failure(new RuntimeException(String.format( + "msg.when is %d should be 0 when processing # %d", + msg.when, msg.what))); + } + if (msg.target != null) { + failure(new RuntimeException(String.format( + "msg.target is NOT null when processing # %d", msg.what))); + } + if (msg.callback != null) { + failure(new RuntimeException(String.format( + "msg.callback is NOT null when processing # %d", msg.what))); + } + } else { + failure(new RuntimeException(String.format( + "Unexpected msg.what is %d" + msg.what))); + } + } + }; + + tester.doTest(1000); + } +} diff --git a/src/main/java/android/os/Messenger.java b/src/main/java/android/os/Messenger.java new file mode 100644 index 0000000..f362f56 --- /dev/null +++ b/src/main/java/android/os/Messenger.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2006 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 android.os; + +/** + * Reference to a Handler, which others can use to send messages to it. + * This allows for the implementation of message-based communication across + * processes, by creating a Messenger pointing to a Handler in one process, + * and handing that Messenger to another process. + * + *

Note: the implementation underneath is just a simple wrapper around + * a {@link Binder} that is used to perform the communication. This means + * semantically you should treat it as such: this class does not impact process + * lifecycle management (you must be using some higher-level component to tell + * the system that your process needs to continue running), the connection will + * break if your process goes away for any reason, etc.

+ */ +public final class Messenger implements Parcelable { + private final IMessenger mTarget; + + /** + * Create a new Messenger pointing to the given Handler. Any Message + * objects sent through this Messenger will appear in the Handler as if + * {@link Handler#sendMessage(Message) Handler.sendMessage(Message)} had + * been called directly. + * + * @param target The Handler that will receive sent messages. + */ + public Messenger(Handler target) { + mTarget = target.getIMessenger(); + } + + /** + * Send a Message to this Messenger's Handler. + * + * @param message The Message to send. Usually retrieved through + * {@link Message#obtain() Message.obtain()}. + * + * @throws RemoteException Throws DeadObjectException if the target + * Handler no longer exists. + */ + public void send(Message message) throws RemoteException { + mTarget.send(message); + } + + /** + * Retrieve the IBinder that this Messenger is using to communicate with + * its associated Handler. + * + * @return Returns the IBinder backing this Messenger. + */ + public IBinder getBinder() { + return mTarget.asBinder(); + } + + /** + * Comparison operator on two Messenger objects, such that true + * is returned then they both point to the same Handler. + */ + public boolean equals(Object otherObj) { + if (otherObj == null) { + return false; + } + try { + return mTarget.asBinder().equals(((Messenger)otherObj) + .mTarget.asBinder()); + } catch (ClassCastException e) { + } + return false; + } + + public int hashCode() { + return mTarget.asBinder().hashCode(); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeStrongBinder(mTarget.asBinder()); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public Messenger createFromParcel(Parcel in) { + IBinder target = in.readStrongBinder(); + return target != null ? new Messenger(target) : null; + } + + public Messenger[] newArray(int size) { + return new Messenger[size]; + } + }; + + /** + * Convenience function for writing either a Messenger or null pointer to + * a Parcel. You must use this with {@link #readMessengerOrNullFromParcel} + * for later reading it. + * + * @param messenger The Messenger to write, or null. + * @param out Where to write the Messenger. + */ + public static void writeMessengerOrNullToParcel(Messenger messenger, + Parcel out) { + out.writeStrongBinder(messenger != null ? messenger.mTarget.asBinder() + : null); + } + + /** + * Convenience function for reading either a Messenger or null pointer from + * a Parcel. You must have previously written the Messenger with + * {@link #writeMessengerOrNullToParcel}. + * + * @param in The Parcel containing the written Messenger. + * + * @return Returns the Messenger read from the Parcel, or null if null had + * been written. + */ + public static Messenger readMessengerOrNullFromParcel(Parcel in) { + IBinder b = in.readStrongBinder(); + return b != null ? new Messenger(b) : null; + } + + /** + * Create a Messenger from a raw IBinder, which had previously been + * retrieved with {@link #getBinder}. + * + * @param target The IBinder this Messenger should communicate with. + */ + public Messenger(IBinder target) { + mTarget = IMessenger.Stub.asInterface(target); + } +} diff --git a/src/main/java/android/os/MessengerService.java b/src/main/java/android/os/MessengerService.java new file mode 100644 index 0000000..f15e134 --- /dev/null +++ b/src/main/java/android/os/MessengerService.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.app.Service; +import android.content.Intent; +import android.os.RemoteException; +import android.os.IBinder; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; + +public class MessengerService extends Service { + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + Message reply = Message.obtain(); + reply.copyFrom(msg); + try { + msg.replyTo.send(reply); + } catch (RemoteException e) { + } + } + }; + + private final Messenger mMessenger = new Messenger(mHandler); + + public MessengerService() { + } + + @Override + public IBinder onBind(Intent intent) { + return mMessenger.getBinder(); + } +} + diff --git a/src/main/java/android/os/MessengerTest.java b/src/main/java/android/os/MessengerTest.java new file mode 100644 index 0000000..473ffe2 --- /dev/null +++ b/src/main/java/android/os/MessengerTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.RemoteException; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.MediumTest; + +public class MessengerTest extends AndroidTestCase { + private Messenger mServiceMessenger; + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName name, IBinder service) { + synchronized (MessengerTest.this) { + mServiceMessenger = new Messenger(service); + MessengerTest.this.notifyAll(); + } + } + public void onServiceDisconnected(ComponentName name) { + mServiceMessenger = null; + } + }; + + private class TestThread extends TestHandlerThread { + private Handler mTestHandler; + private Messenger mTestMessenger; + + public void go() { + synchronized (MessengerTest.this) { + mTestHandler = new Handler() { + public void handleMessage(Message msg) { + TestThread.this.handleMessage(msg); + } + }; + mTestMessenger = new Messenger(mTestHandler); + TestThread.this.executeTest(); + } + } + + public void executeTest() { + Message msg = Message.obtain(); + msg.arg1 = 100; + msg.arg2 = 1000; + msg.replyTo = mTestMessenger; + try { + mServiceMessenger.send(msg); + } catch (RemoteException e) { + } + } + + public void handleMessage(Message msg) { + if (msg.arg1 != 100) { + failure(new RuntimeException( + "Message.arg1 is not 100: " + msg.arg1)); + return; + } + if (msg.arg2 != 1000) { + failure(new RuntimeException( + "Message.arg2 is not 1000: " + msg.arg2)); + return; + } + if (!mTestMessenger.equals(msg.replyTo)) { + failure(new RuntimeException( + "Message.replyTo is not me: " + msg.replyTo)); + return; + } + success(); + } + }; + + @Override + protected void setUp() throws Exception { + super.setUp(); + getContext().bindService(new Intent(mContext, MessengerService.class), + mConnection, Context.BIND_AUTO_CREATE); + synchronized (this) { + while (mServiceMessenger == null) { + try { + wait(); + } catch (InterruptedException e) { + } + } + } + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + getContext().unbindService(mConnection); + } + + @MediumTest + public void testSend() { + (new TestThread()).doTest(1000); + + } +} diff --git a/src/main/java/android/os/NetworkOnMainThreadException.java b/src/main/java/android/os/NetworkOnMainThreadException.java new file mode 100644 index 0000000..dd8c66c --- /dev/null +++ b/src/main/java/android/os/NetworkOnMainThreadException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010 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 android.os; + +/** + * The exception that is thrown when an application attempts + * to perform a networking operation on its main thread. + * + *

This is only thrown for applications targeting the Honeycomb + * SDK or higher. Applications targeting earlier SDK versions + * are allowed to do networking on their main event loop threads, + * but it's heavily discouraged. See the document + * + * Designing for Responsiveness. + * + *

Also see {@link StrictMode}. + */ +public class NetworkOnMainThreadException extends RuntimeException { +} diff --git a/src/main/java/android/os/NullVibrator.java b/src/main/java/android/os/NullVibrator.java new file mode 100644 index 0000000..f14d965 --- /dev/null +++ b/src/main/java/android/os/NullVibrator.java @@ -0,0 +1,63 @@ +/* + * 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 android.os; + +import android.media.AudioAttributes; + +/** + * Vibrator implementation that does nothing. + * + * @hide + */ +public class NullVibrator extends Vibrator { + private static final NullVibrator sInstance = new NullVibrator(); + + private NullVibrator() { + } + + public static NullVibrator getInstance() { + return sInstance; + } + + @Override + public boolean hasVibrator() { + return false; + } + + /** + * @hide + */ + @Override + public void vibrate(int uid, String opPkg, long milliseconds, AudioAttributes attributes) { + vibrate(milliseconds); + } + + /** + * @hide + */ + @Override + public void vibrate(int uid, String opPkg, long[] pattern, int repeat, + AudioAttributes attributes) { + if (repeat >= pattern.length) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + @Override + public void cancel() { + } +} diff --git a/src/main/java/android/os/OperationCanceledException.java b/src/main/java/android/os/OperationCanceledException.java new file mode 100644 index 0000000..b0cd663 --- /dev/null +++ b/src/main/java/android/os/OperationCanceledException.java @@ -0,0 +1,33 @@ +/* + * 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 android.os; + + +/** + * An exception type that is thrown when an operation in progress is canceled. + * + * @see CancellationSignal + */ +public class OperationCanceledException extends RuntimeException { + public OperationCanceledException() { + this(null); + } + + public OperationCanceledException(String message) { + super(message != null ? message : "The operation has been canceled."); + } +} diff --git a/src/main/java/android/os/OsTests.java b/src/main/java/android/os/OsTests.java new file mode 100644 index 0000000..582bf1a --- /dev/null +++ b/src/main/java/android/os/OsTests.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2006 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 android.os; + +import com.google.android.collect.Lists; +import junit.framework.TestSuite; + +import java.util.Enumeration; +import java.util.List; + +public class OsTests { + public static TestSuite suite() { + TestSuite suite = new TestSuite(OsTests.class.getName()); + + suite.addTestSuite(AidlTest.class); + suite.addTestSuite(BroadcasterTest.class); + suite.addTestSuite(FileObserverTest.class); + suite.addTestSuite(IdleHandlerTest.class); + suite.addTestSuite(MessageQueueTest.class); + suite.addTestSuite(MessengerTest.class); + suite.addTestSuite(SystemPropertiesTest.class); + + return suite; + } +} diff --git a/src/main/java/android/os/Parcel.java b/src/main/java/android/os/Parcel.java new file mode 100644 index 0000000..3d5215b --- /dev/null +++ b/src/main/java/android/os/Parcel.java @@ -0,0 +1,2563 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Size; +import android.util.SizeF; +import android.util.SparseArray; +import android.util.SparseBooleanArray; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Container for a message (data and object references) that can + * be sent through an IBinder. A Parcel can contain both flattened data + * that will be unflattened on the other side of the IPC (using the various + * methods here for writing specific types, or the general + * {@link Parcelable} interface), and references to live {@link IBinder} + * objects that will result in the other side receiving a proxy IBinder + * connected with the original IBinder in the Parcel. + * + *

Parcel is not a general-purpose + * serialization mechanism. This class (and the corresponding + * {@link Parcelable} API for placing arbitrary objects into a Parcel) is + * designed as a high-performance IPC transport. As such, it is not + * appropriate to place any Parcel data in to persistent storage: changes + * in the underlying implementation of any of the data in the Parcel can + * render older data unreadable.

+ * + *

The bulk of the Parcel API revolves around reading and writing data + * of various types. There are six major classes of such functions available.

+ * + *

Primitives

+ * + *

The most basic data functions are for writing and reading primitive + * data types: {@link #writeByte}, {@link #readByte}, {@link #writeDouble}, + * {@link #readDouble}, {@link #writeFloat}, {@link #readFloat}, {@link #writeInt}, + * {@link #readInt}, {@link #writeLong}, {@link #readLong}, + * {@link #writeString}, {@link #readString}. Most other + * data operations are built on top of these. The given data is written and + * read using the endianess of the host CPU.

+ * + *

Primitive Arrays

+ * + *

There are a variety of methods for reading and writing raw arrays + * of primitive objects, which generally result in writing a 4-byte length + * followed by the primitive data items. The methods for reading can either + * read the data into an existing array, or create and return a new array. + * These available types are:

+ * + *
    + *
  • {@link #writeBooleanArray(boolean[])}, + * {@link #readBooleanArray(boolean[])}, {@link #createBooleanArray()} + *
  • {@link #writeByteArray(byte[])}, + * {@link #writeByteArray(byte[], int, int)}, {@link #readByteArray(byte[])}, + * {@link #createByteArray()} + *
  • {@link #writeCharArray(char[])}, {@link #readCharArray(char[])}, + * {@link #createCharArray()} + *
  • {@link #writeDoubleArray(double[])}, {@link #readDoubleArray(double[])}, + * {@link #createDoubleArray()} + *
  • {@link #writeFloatArray(float[])}, {@link #readFloatArray(float[])}, + * {@link #createFloatArray()} + *
  • {@link #writeIntArray(int[])}, {@link #readIntArray(int[])}, + * {@link #createIntArray()} + *
  • {@link #writeLongArray(long[])}, {@link #readLongArray(long[])}, + * {@link #createLongArray()} + *
  • {@link #writeStringArray(String[])}, {@link #readStringArray(String[])}, + * {@link #createStringArray()}. + *
  • {@link #writeSparseBooleanArray(SparseBooleanArray)}, + * {@link #readSparseBooleanArray()}. + *
+ * + *

Parcelables

+ * + *

The {@link Parcelable} protocol provides an extremely efficient (but + * low-level) protocol for objects to write and read themselves from Parcels. + * You can use the direct methods {@link #writeParcelable(Parcelable, int)} + * and {@link #readParcelable(ClassLoader)} or + * {@link #writeParcelableArray} and + * {@link #readParcelableArray(ClassLoader)} to write or read. These + * methods write both the class type and its data to the Parcel, allowing + * that class to be reconstructed from the appropriate class loader when + * later reading.

+ * + *

There are also some methods that provide a more efficient way to work + * with Parcelables: {@link #writeTypedArray}, + * {@link #writeTypedList(List)}, + * {@link #readTypedArray} and {@link #readTypedList}. These methods + * do not write the class information of the original object: instead, the + * caller of the read function must know what type to expect and pass in the + * appropriate {@link Parcelable.Creator Parcelable.Creator} instead to + * properly construct the new object and read its data. (To more efficient + * write and read a single Parceable object, you can directly call + * {@link Parcelable#writeToParcel Parcelable.writeToParcel} and + * {@link Parcelable.Creator#createFromParcel Parcelable.Creator.createFromParcel} + * yourself.)

+ * + *

Bundles

+ * + *

A special type-safe container, called {@link Bundle}, is available + * for key/value maps of heterogeneous values. This has many optimizations + * for improved performance when reading and writing data, and its type-safe + * API avoids difficult to debug type errors when finally marshalling the + * data contents into a Parcel. The methods to use are + * {@link #writeBundle(Bundle)}, {@link #readBundle()}, and + * {@link #readBundle(ClassLoader)}. + * + *

Active Objects

+ * + *

An unusual feature of Parcel is the ability to read and write active + * objects. For these objects the actual contents of the object is not + * written, rather a special token referencing the object is written. When + * reading the object back from the Parcel, you do not get a new instance of + * the object, but rather a handle that operates on the exact same object that + * was originally written. There are two forms of active objects available.

+ * + *

{@link Binder} objects are a core facility of Android's general cross-process + * communication system. The {@link IBinder} interface describes an abstract + * protocol with a Binder object. Any such interface can be written in to + * a Parcel, and upon reading you will receive either the original object + * implementing that interface or a special proxy implementation + * that communicates calls back to the original object. The methods to use are + * {@link #writeStrongBinder(IBinder)}, + * {@link #writeStrongInterface(IInterface)}, {@link #readStrongBinder()}, + * {@link #writeBinderArray(IBinder[])}, {@link #readBinderArray(IBinder[])}, + * {@link #createBinderArray()}, + * {@link #writeBinderList(List)}, {@link #readBinderList(List)}, + * {@link #createBinderArrayList()}.

+ * + *

FileDescriptor objects, representing raw Linux file descriptor identifiers, + * can be written and {@link ParcelFileDescriptor} objects returned to operate + * on the original file descriptor. The returned file descriptor is a dup + * of the original file descriptor: the object and fd is different, but + * operating on the same underlying file stream, with the same position, etc. + * The methods to use are {@link #writeFileDescriptor(FileDescriptor)}, + * {@link #readFileDescriptor()}. + * + *

Untyped Containers

+ * + *

A final class of methods are for writing and reading standard Java + * containers of arbitrary types. These all revolve around the + * {@link #writeValue(Object)} and {@link #readValue(ClassLoader)} methods + * which define the types of objects allowed. The container methods are + * {@link #writeArray(Object[])}, {@link #readArray(ClassLoader)}, + * {@link #writeList(List)}, {@link #readList(List, ClassLoader)}, + * {@link #readArrayList(ClassLoader)}, + * {@link #writeMap(Map)}, {@link #readMap(Map, ClassLoader)}, + * {@link #writeSparseArray(SparseArray)}, + * {@link #readSparseArray(ClassLoader)}. + */ +public final class Parcel { + private static final boolean DEBUG_RECYCLE = false; + private static final boolean DEBUG_ARRAY_MAP = false; + private static final String TAG = "Parcel"; + + @SuppressWarnings({"UnusedDeclaration"}) + private long mNativePtr; // used by native code + + /** + * Flag indicating if {@link #mNativePtr} was allocated by this object, + * indicating that we're responsible for its lifecycle. + */ + private boolean mOwnsNativeParcelObject; + + private RuntimeException mStack; + + private static final int POOL_SIZE = 6; + private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE]; + private static final Parcel[] sHolderPool = new Parcel[POOL_SIZE]; + + private static final int VAL_NULL = -1; + private static final int VAL_STRING = 0; + private static final int VAL_INTEGER = 1; + private static final int VAL_MAP = 2; + private static final int VAL_BUNDLE = 3; + private static final int VAL_PARCELABLE = 4; + private static final int VAL_SHORT = 5; + private static final int VAL_LONG = 6; + private static final int VAL_FLOAT = 7; + private static final int VAL_DOUBLE = 8; + private static final int VAL_BOOLEAN = 9; + private static final int VAL_CHARSEQUENCE = 10; + private static final int VAL_LIST = 11; + private static final int VAL_SPARSEARRAY = 12; + private static final int VAL_BYTEARRAY = 13; + private static final int VAL_STRINGARRAY = 14; + private static final int VAL_IBINDER = 15; + private static final int VAL_PARCELABLEARRAY = 16; + private static final int VAL_OBJECTARRAY = 17; + private static final int VAL_INTARRAY = 18; + private static final int VAL_LONGARRAY = 19; + private static final int VAL_BYTE = 20; + private static final int VAL_SERIALIZABLE = 21; + private static final int VAL_SPARSEBOOLEANARRAY = 22; + private static final int VAL_BOOLEANARRAY = 23; + private static final int VAL_CHARSEQUENCEARRAY = 24; + private static final int VAL_PERSISTABLEBUNDLE = 25; + private static final int VAL_SIZE = 26; + private static final int VAL_SIZEF = 27; + + // The initial int32 in a Binder call's reply Parcel header: + private static final int EX_SECURITY = -1; + private static final int EX_BAD_PARCELABLE = -2; + private static final int EX_ILLEGAL_ARGUMENT = -3; + private static final int EX_NULL_POINTER = -4; + private static final int EX_ILLEGAL_STATE = -5; + private static final int EX_NETWORK_MAIN_THREAD = -6; + private static final int EX_UNSUPPORTED_OPERATION = -7; + private static final int EX_HAS_REPLY_HEADER = -128; // special; see below + + private static native int nativeDataSize(long nativePtr); + private static native int nativeDataAvail(long nativePtr); + private static native int nativeDataPosition(long nativePtr); + private static native int nativeDataCapacity(long nativePtr); + private static native void nativeSetDataSize(long nativePtr, int size); + private static native void nativeSetDataPosition(long nativePtr, int pos); + private static native void nativeSetDataCapacity(long nativePtr, int size); + + private static native boolean nativePushAllowFds(long nativePtr, boolean allowFds); + private static native void nativeRestoreAllowFds(long nativePtr, boolean lastValue); + + private static native void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len); + private static native void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len); + private static native void nativeWriteInt(long nativePtr, int val); + private static native void nativeWriteLong(long nativePtr, long val); + private static native void nativeWriteFloat(long nativePtr, float val); + private static native void nativeWriteDouble(long nativePtr, double val); + private static native void nativeWriteString(long nativePtr, String val); + private static native void nativeWriteStrongBinder(long nativePtr, IBinder val); + private static native void nativeWriteFileDescriptor(long nativePtr, FileDescriptor val); + + private static native byte[] nativeCreateByteArray(long nativePtr); + private static native byte[] nativeReadBlob(long nativePtr); + private static native int nativeReadInt(long nativePtr); + private static native long nativeReadLong(long nativePtr); + private static native float nativeReadFloat(long nativePtr); + private static native double nativeReadDouble(long nativePtr); + private static native String nativeReadString(long nativePtr); + private static native IBinder nativeReadStrongBinder(long nativePtr); + private static native FileDescriptor nativeReadFileDescriptor(long nativePtr); + + private static native long nativeCreate(); + private static native void nativeFreeBuffer(long nativePtr); + private static native void nativeDestroy(long nativePtr); + + private static native byte[] nativeMarshall(long nativePtr); + private static native void nativeUnmarshall( + long nativePtr, byte[] data, int offset, int length); + private static native void nativeAppendFrom( + long thisNativePtr, long otherNativePtr, int offset, int length); + private static native boolean nativeHasFileDescriptors(long nativePtr); + private static native void nativeWriteInterfaceToken(long nativePtr, String interfaceName); + private static native void nativeEnforceInterface(long nativePtr, String interfaceName); + + public final static Parcelable.Creator STRING_CREATOR + = new Parcelable.Creator() { + public String createFromParcel(Parcel source) { + return source.readString(); + } + public String[] newArray(int size) { + return new String[size]; + } + }; + + /** + * Retrieve a new Parcel object from the pool. + */ + public static Parcel obtain() { + final Parcel[] pool = sOwnedPool; + synchronized (pool) { + Parcel p; + for (int i=0; i= {@link #dataSize}. The difference between it and dataSize() is the + * amount of room left until the parcel needs to re-allocate its + * data buffer. + */ + public final int dataCapacity() { + return nativeDataCapacity(mNativePtr); + } + + /** + * Change the amount of data in the parcel. Can be either smaller or + * larger than the current size. If larger than the current capacity, + * more memory will be allocated. + * + * @param size The new number of bytes in the Parcel. + */ + public final void setDataSize(int size) { + nativeSetDataSize(mNativePtr, size); + } + + /** + * Move the current read/write position in the parcel. + * @param pos New offset in the parcel; must be between 0 and + * {@link #dataSize}. + */ + public final void setDataPosition(int pos) { + nativeSetDataPosition(mNativePtr, pos); + } + + /** + * Change the capacity (current available space) of the parcel. + * + * @param size The new capacity of the parcel, in bytes. Can not be + * less than {@link #dataSize} -- that is, you can not drop existing data + * with this method. + */ + public final void setDataCapacity(int size) { + nativeSetDataCapacity(mNativePtr, size); + } + + /** @hide */ + public final boolean pushAllowFds(boolean allowFds) { + return nativePushAllowFds(mNativePtr, allowFds); + } + + /** @hide */ + public final void restoreAllowFds(boolean lastValue) { + nativeRestoreAllowFds(mNativePtr, lastValue); + } + + /** + * Returns the raw bytes of the parcel. + * + *

The data you retrieve here must not + * be placed in any kind of persistent storage (on local disk, across + * a network, etc). For that, you should use standard serialization + * or another kind of general serialization mechanism. The Parcel + * marshalled representation is highly optimized for local IPC, and as + * such does not attempt to maintain compatibility with data created + * in different versions of the platform. + */ + public final byte[] marshall() { + return nativeMarshall(mNativePtr); + } + + /** + * Set the bytes in data to be the raw bytes of this Parcel. + */ + public final void unmarshall(byte[] data, int offset, int length) { + nativeUnmarshall(mNativePtr, data, offset, length); + } + + public final void appendFrom(Parcel parcel, int offset, int length) { + nativeAppendFrom(mNativePtr, parcel.mNativePtr, offset, length); + } + + /** + * Report whether the parcel contains any marshalled file descriptors. + */ + public final boolean hasFileDescriptors() { + return nativeHasFileDescriptors(mNativePtr); + } + + /** + * Store or read an IBinder interface token in the parcel at the current + * {@link #dataPosition}. This is used to validate that the marshalled + * transaction is intended for the target interface. + */ + public final void writeInterfaceToken(String interfaceName) { + nativeWriteInterfaceToken(mNativePtr, interfaceName); + } + + public final void enforceInterface(String interfaceName) { + nativeEnforceInterface(mNativePtr, interfaceName); + } + + /** + * Write a byte array into the parcel at the current {@link #dataPosition}, + * growing {@link #dataCapacity} if needed. + * @param b Bytes to place into the parcel. + */ + public final void writeByteArray(byte[] b) { + writeByteArray(b, 0, (b != null) ? b.length : 0); + } + + /** + * Write a byte array into the parcel at the current {@link #dataPosition}, + * growing {@link #dataCapacity} if needed. + * @param b Bytes to place into the parcel. + * @param offset Index of first byte to be written. + * @param len Number of bytes to write. + */ + public final void writeByteArray(byte[] b, int offset, int len) { + if (b == null) { + writeInt(-1); + return; + } + Arrays.checkOffsetAndCount(b.length, offset, len); + nativeWriteByteArray(mNativePtr, b, offset, len); + } + + /** + * Write a blob of data into the parcel at the current {@link #dataPosition}, + * growing {@link #dataCapacity} if needed. + * @param b Bytes to place into the parcel. + * {@hide} + * {@SystemApi} + */ + public final void writeBlob(byte[] b) { + nativeWriteBlob(mNativePtr, b, 0, (b != null) ? b.length : 0); + } + + /** + * Write an integer value into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writeInt(int val) { + nativeWriteInt(mNativePtr, val); + } + + /** + * Write a long integer value into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writeLong(long val) { + nativeWriteLong(mNativePtr, val); + } + + /** + * Write a floating point value into the parcel at the current + * dataPosition(), growing dataCapacity() if needed. + */ + public final void writeFloat(float val) { + nativeWriteFloat(mNativePtr, val); + } + + /** + * Write a double precision floating point value into the parcel at the + * current dataPosition(), growing dataCapacity() if needed. + */ + public final void writeDouble(double val) { + nativeWriteDouble(mNativePtr, val); + } + + /** + * Write a string value into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writeString(String val) { + nativeWriteString(mNativePtr, val); + } + + /** + * Write a CharSequence value into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + * @hide + */ + public final void writeCharSequence(CharSequence val) { + TextUtils.writeToParcel(val, this, 0); + } + + /** + * Write an object into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writeStrongBinder(IBinder val) { + nativeWriteStrongBinder(mNativePtr, val); + } + + /** + * Write an object into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writeStrongInterface(IInterface val) { + writeStrongBinder(val == null ? null : val.asBinder()); + } + + /** + * Write a FileDescriptor into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + * + *

The file descriptor will not be closed, which may + * result in file descriptor leaks when objects are returned from Binder + * calls. Use {@link ParcelFileDescriptor#writeToParcel} instead, which + * accepts contextual flags and will close the original file descriptor + * if {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE} is set.

+ */ + public final void writeFileDescriptor(FileDescriptor val) { + nativeWriteFileDescriptor(mNativePtr, val); + } + + /** + * Write a byte value into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writeByte(byte val) { + writeInt(val); + } + + /** + * Please use {@link #writeBundle} instead. Flattens a Map into the parcel + * at the current dataPosition(), + * growing dataCapacity() if needed. The Map keys must be String objects. + * The Map values are written using {@link #writeValue} and must follow + * the specification there. + * + *

It is strongly recommended to use {@link #writeBundle} instead of + * this method, since the Bundle class provides a type-safe API that + * allows you to avoid mysterious type errors at the point of marshalling. + */ + public final void writeMap(Map val) { + writeMapInternal((Map) val); + } + + /** + * Flatten a Map into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. The Map keys must be String objects. + */ + /* package */ void writeMapInternal(Map val) { + if (val == null) { + writeInt(-1); + return; + } + Set> entries = val.entrySet(); + writeInt(entries.size()); + for (Map.Entry e : entries) { + writeValue(e.getKey()); + writeValue(e.getValue()); + } + } + + /** + * Flatten an ArrayMap into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. The Map keys must be String objects. + */ + /* package */ void writeArrayMapInternal(ArrayMap val) { + if (val == null) { + writeInt(-1); + return; + } + final int N = val.size(); + writeInt(N); + if (DEBUG_ARRAY_MAP) { + RuntimeException here = new RuntimeException("here"); + here.fillInStackTrace(); + Log.d(TAG, "Writing " + N + " ArrayMap entries", here); + } + int startPos; + for (int i=0; i val) { + writeArrayMapInternal(val); + } + + /** + * Flatten a Bundle into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writeBundle(Bundle val) { + if (val == null) { + writeInt(-1); + return; + } + + val.writeToParcel(this, 0); + } + + /** + * Flatten a PersistableBundle into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writePersistableBundle(PersistableBundle val) { + if (val == null) { + writeInt(-1); + return; + } + + val.writeToParcel(this, 0); + } + + /** + * Flatten a Size into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writeSize(Size val) { + writeInt(val.getWidth()); + writeInt(val.getHeight()); + } + + /** + * Flatten a SizeF into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. + */ + public final void writeSizeF(SizeF val) { + writeFloat(val.getWidth()); + writeFloat(val.getHeight()); + } + + /** + * Flatten a List into the parcel at the current dataPosition(), growing + * dataCapacity() if needed. The List values are written using + * {@link #writeValue} and must follow the specification there. + */ + public final void writeList(List val) { + if (val == null) { + writeInt(-1); + return; + } + int N = val.size(); + int i=0; + writeInt(N); + while (i < N) { + writeValue(val.get(i)); + i++; + } + } + + /** + * Flatten an Object array into the parcel at the current dataPosition(), + * growing dataCapacity() if needed. The array values are written using + * {@link #writeValue} and must follow the specification there. + */ + public final void writeArray(Object[] val) { + if (val == null) { + writeInt(-1); + return; + } + int N = val.length; + int i=0; + writeInt(N); + while (i < N) { + writeValue(val[i]); + i++; + } + } + + /** + * Flatten a generic SparseArray into the parcel at the current + * dataPosition(), growing dataCapacity() if needed. The SparseArray + * values are written using {@link #writeValue} and must follow the + * specification there. + */ + public final void writeSparseArray(SparseArray val) { + if (val == null) { + writeInt(-1); + return; + } + int N = val.size(); + writeInt(N); + int i=0; + while (i < N) { + writeInt(val.keyAt(i)); + writeValue(val.valueAt(i)); + i++; + } + } + + public final void writeSparseBooleanArray(SparseBooleanArray val) { + if (val == null) { + writeInt(-1); + return; + } + int N = val.size(); + writeInt(N); + int i=0; + while (i < N) { + writeInt(val.keyAt(i)); + writeByte((byte)(val.valueAt(i) ? 1 : 0)); + i++; + } + } + + public final void writeBooleanArray(boolean[] val) { + if (val != null) { + int N = val.length; + writeInt(N); + for (int i=0; i>2 as a fast divide-by-4 works in the create*Array() functions + // because dataAvail() will never return a negative number. 4 is + // the size of a stored boolean in the stream. + if (N >= 0 && N <= (dataAvail() >> 2)) { + boolean[] val = new boolean[N]; + for (int i=0; i= 0 && N <= (dataAvail() >> 2)) { + char[] val = new char[N]; + for (int i=0; i= 0 && N <= (dataAvail() >> 2)) { + int[] val = new int[N]; + for (int i=0; i>3 because stored longs are 64 bits + if (N >= 0 && N <= (dataAvail() >> 3)) { + long[] val = new long[N]; + for (int i=0; i>2 because stored floats are 4 bytes + if (N >= 0 && N <= (dataAvail() >> 2)) { + float[] val = new float[N]; + for (int i=0; i>3 because stored doubles are 8 bytes + if (N >= 0 && N <= (dataAvail() >> 3)) { + double[] val = new double[N]; + for (int i=0; i= 0) { + String[] val = new String[N]; + for (int i=0; i= 0) { + IBinder[] val = new IBinder[N]; + for (int i=0; i void writeTypedList(List val) { + if (val == null) { + writeInt(-1); + return; + } + int N = val.size(); + int i=0; + writeInt(N); + while (i < N) { + T item = val.get(i); + if (item != null) { + writeInt(1); + item.writeToParcel(this, 0); + } else { + writeInt(0); + } + i++; + } + } + + /** + * Flatten a List containing String objects into the parcel, at + * the current dataPosition() and growing dataCapacity() if needed. They + * can later be retrieved with {@link #createStringArrayList} or + * {@link #readStringList}. + * + * @param val The list of strings to be written. + * + * @see #createStringArrayList + * @see #readStringList + */ + public final void writeStringList(List val) { + if (val == null) { + writeInt(-1); + return; + } + int N = val.size(); + int i=0; + writeInt(N); + while (i < N) { + writeString(val.get(i)); + i++; + } + } + + /** + * Flatten a List containing IBinder objects into the parcel, at + * the current dataPosition() and growing dataCapacity() if needed. They + * can later be retrieved with {@link #createBinderArrayList} or + * {@link #readBinderList}. + * + * @param val The list of strings to be written. + * + * @see #createBinderArrayList + * @see #readBinderList + */ + public final void writeBinderList(List val) { + if (val == null) { + writeInt(-1); + return; + } + int N = val.size(); + int i=0; + writeInt(N); + while (i < N) { + writeStrongBinder(val.get(i)); + i++; + } + } + + /** + * Flatten a heterogeneous array containing a particular object type into + * the parcel, at + * the current dataPosition() and growing dataCapacity() if needed. The + * type of the objects in the array must be one that implements Parcelable. + * Unlike the {@link #writeParcelableArray} method, however, only the + * raw data of the objects is written and not their type, so you must use + * {@link #readTypedArray} with the correct corresponding + * {@link Parcelable.Creator} implementation to unmarshall them. + * + * @param val The array of objects to be written. + * @param parcelableFlags Contextual flags as per + * {@link Parcelable#writeToParcel(Parcel, int) Parcelable.writeToParcel()}. + * + * @see #readTypedArray + * @see #writeParcelableArray + * @see Parcelable.Creator + */ + public final void writeTypedArray(T[] val, + int parcelableFlags) { + if (val != null) { + int N = val.length; + writeInt(N); + for (int i=0; i + *
  • null + *
  • String + *
  • Byte + *
  • Short + *
  • Integer + *
  • Long + *
  • Float + *
  • Double + *
  • Boolean + *
  • String[] + *
  • boolean[] + *
  • byte[] + *
  • int[] + *
  • long[] + *
  • Object[] (supporting objects of the same type defined here). + *
  • {@link Bundle} + *
  • Map (as supported by {@link #writeMap}). + *
  • Any object that implements the {@link Parcelable} protocol. + *
  • Parcelable[] + *
  • CharSequence (as supported by {@link TextUtils#writeToParcel}). + *
  • List (as supported by {@link #writeList}). + *
  • {@link SparseArray} (as supported by {@link #writeSparseArray(SparseArray)}). + *
  • {@link IBinder} + *
  • Any object that implements Serializable (but see + * {@link #writeSerializable} for caveats). Note that all of the + * previous types have relatively efficient implementations for + * writing to a Parcel; having to rely on the generic serialization + * approach is much less efficient and should be avoided whenever + * possible. + * + * + *

    {@link Parcelable} objects are written with + * {@link Parcelable#writeToParcel} using contextual flags of 0. When + * serializing objects containing {@link ParcelFileDescriptor}s, + * this may result in file descriptor leaks when they are returned from + * Binder calls (where {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE} + * should be used).

    + */ + public final void writeValue(Object v) { + if (v == null) { + writeInt(VAL_NULL); + } else if (v instanceof String) { + writeInt(VAL_STRING); + writeString((String) v); + } else if (v instanceof Integer) { + writeInt(VAL_INTEGER); + writeInt((Integer) v); + } else if (v instanceof Map) { + writeInt(VAL_MAP); + writeMap((Map) v); + } else if (v instanceof Bundle) { + // Must be before Parcelable + writeInt(VAL_BUNDLE); + writeBundle((Bundle) v); + } else if (v instanceof Parcelable) { + writeInt(VAL_PARCELABLE); + writeParcelable((Parcelable) v, 0); + } else if (v instanceof Short) { + writeInt(VAL_SHORT); + writeInt(((Short) v).intValue()); + } else if (v instanceof Long) { + writeInt(VAL_LONG); + writeLong((Long) v); + } else if (v instanceof Float) { + writeInt(VAL_FLOAT); + writeFloat((Float) v); + } else if (v instanceof Double) { + writeInt(VAL_DOUBLE); + writeDouble((Double) v); + } else if (v instanceof Boolean) { + writeInt(VAL_BOOLEAN); + writeInt((Boolean) v ? 1 : 0); + } else if (v instanceof CharSequence) { + // Must be after String + writeInt(VAL_CHARSEQUENCE); + writeCharSequence((CharSequence) v); + } else if (v instanceof List) { + writeInt(VAL_LIST); + writeList((List) v); + } else if (v instanceof SparseArray) { + writeInt(VAL_SPARSEARRAY); + writeSparseArray((SparseArray) v); + } else if (v instanceof boolean[]) { + writeInt(VAL_BOOLEANARRAY); + writeBooleanArray((boolean[]) v); + } else if (v instanceof byte[]) { + writeInt(VAL_BYTEARRAY); + writeByteArray((byte[]) v); + } else if (v instanceof String[]) { + writeInt(VAL_STRINGARRAY); + writeStringArray((String[]) v); + } else if (v instanceof CharSequence[]) { + // Must be after String[] and before Object[] + writeInt(VAL_CHARSEQUENCEARRAY); + writeCharSequenceArray((CharSequence[]) v); + } else if (v instanceof IBinder) { + writeInt(VAL_IBINDER); + writeStrongBinder((IBinder) v); + } else if (v instanceof Parcelable[]) { + writeInt(VAL_PARCELABLEARRAY); + writeParcelableArray((Parcelable[]) v, 0); + } else if (v instanceof int[]) { + writeInt(VAL_INTARRAY); + writeIntArray((int[]) v); + } else if (v instanceof long[]) { + writeInt(VAL_LONGARRAY); + writeLongArray((long[]) v); + } else if (v instanceof Byte) { + writeInt(VAL_BYTE); + writeInt((Byte) v); + } else if (v instanceof PersistableBundle) { + writeInt(VAL_PERSISTABLEBUNDLE); + writePersistableBundle((PersistableBundle) v); + } else if (v instanceof Size) { + writeInt(VAL_SIZE); + writeSize((Size) v); + } else if (v instanceof SizeF) { + writeInt(VAL_SIZEF); + writeSizeF((SizeF) v); + } else { + Class clazz = v.getClass(); + if (clazz.isArray() && clazz.getComponentType() == Object.class) { + // Only pure Object[] are written here, Other arrays of non-primitive types are + // handled by serialization as this does not record the component type. + writeInt(VAL_OBJECTARRAY); + writeArray((Object[]) v); + } else if (v instanceof Serializable) { + // Must be last + writeInt(VAL_SERIALIZABLE); + writeSerializable((Serializable) v); + } else { + throw new RuntimeException("Parcel: unable to marshal value " + v); + } + } + } + + /** + * Flatten the name of the class of the Parcelable and its contents + * into the parcel. + * + * @param p The Parcelable object to be written. + * @param parcelableFlags Contextual flags as per + * {@link Parcelable#writeToParcel(Parcel, int) Parcelable.writeToParcel()}. + */ + public final void writeParcelable(Parcelable p, int parcelableFlags) { + if (p == null) { + writeString(null); + return; + } + String name = p.getClass().getName(); + writeString(name); + p.writeToParcel(this, parcelableFlags); + } + + /** @hide */ + public final void writeParcelableCreator(Parcelable p) { + String name = p.getClass().getName(); + writeString(name); + } + + /** + * Write a generic serializable object in to a Parcel. It is strongly + * recommended that this method be avoided, since the serialization + * overhead is extremely large, and this approach will be much slower than + * using the other approaches to writing data in to a Parcel. + */ + public final void writeSerializable(Serializable s) { + if (s == null) { + writeString(null); + return; + } + String name = s.getClass().getName(); + writeString(name); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(s); + oos.close(); + + writeByteArray(baos.toByteArray()); + } catch (IOException ioe) { + throw new RuntimeException("Parcelable encountered " + + "IOException writing serializable object (name = " + name + + ")", ioe); + } + } + + /** + * Special function for writing an exception result at the header of + * a parcel, to be used when returning an exception from a transaction. + * Note that this currently only supports a few exception types; any other + * exception will be re-thrown by this function as a RuntimeException + * (to be caught by the system's last-resort exception handling when + * dispatching a transaction). + * + *

    The supported exception types are: + *

      + *
    • {@link BadParcelableException} + *
    • {@link IllegalArgumentException} + *
    • {@link IllegalStateException} + *
    • {@link NullPointerException} + *
    • {@link SecurityException} + *
    • {@link NetworkOnMainThreadException} + *
    + * + * @param e The Exception to be written. + * + * @see #writeNoException + * @see #readException + */ + public final void writeException(Exception e) { + int code = 0; + if (e instanceof SecurityException) { + code = EX_SECURITY; + } else if (e instanceof BadParcelableException) { + code = EX_BAD_PARCELABLE; + } else if (e instanceof IllegalArgumentException) { + code = EX_ILLEGAL_ARGUMENT; + } else if (e instanceof NullPointerException) { + code = EX_NULL_POINTER; + } else if (e instanceof IllegalStateException) { + code = EX_ILLEGAL_STATE; + } else if (e instanceof NetworkOnMainThreadException) { + code = EX_NETWORK_MAIN_THREAD; + } else if (e instanceof UnsupportedOperationException) { + code = EX_UNSUPPORTED_OPERATION; + } + writeInt(code); + StrictMode.clearGatheredViolations(); + if (code == 0) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new RuntimeException(e); + } + writeString(e.getMessage()); + } + + /** + * Special function for writing information at the front of the Parcel + * indicating that no exception occurred. + * + * @see #writeException + * @see #readException + */ + public final void writeNoException() { + // Despite the name of this function ("write no exception"), + // it should instead be thought of as "write the RPC response + // header", but because this function name is written out by + // the AIDL compiler, we're not going to rename it. + // + // The response header, in the non-exception case (see also + // writeException above, also called by the AIDL compiler), is + // either a 0 (the default case), or EX_HAS_REPLY_HEADER if + // StrictMode has gathered up violations that have occurred + // during a Binder call, in which case we write out the number + // of violations and their details, serialized, before the + // actual RPC respons data. The receiving end of this is + // readException(), below. + if (StrictMode.hasGatheredViolations()) { + writeInt(EX_HAS_REPLY_HEADER); + final int sizePosition = dataPosition(); + writeInt(0); // total size of fat header, to be filled in later + StrictMode.writeGatheredViolationsToParcel(this); + final int payloadPosition = dataPosition(); + setDataPosition(sizePosition); + writeInt(payloadPosition - sizePosition); // header size + setDataPosition(payloadPosition); + } else { + writeInt(0); + } + } + + /** + * Special function for reading an exception result from the header of + * a parcel, to be used after receiving the result of a transaction. This + * will throw the exception for you if it had been written to the Parcel, + * otherwise return and let you read the normal result data from the Parcel. + * + * @see #writeException + * @see #writeNoException + */ + public final void readException() { + int code = readExceptionCode(); + if (code != 0) { + String msg = readString(); + readException(code, msg); + } + } + + /** + * Parses the header of a Binder call's response Parcel and + * returns the exception code. Deals with lite or fat headers. + * In the common successful case, this header is generally zero. + * In less common cases, it's a small negative number and will be + * followed by an error string. + * + * This exists purely for android.database.DatabaseUtils and + * insulating it from having to handle fat headers as returned by + * e.g. StrictMode-induced RPC responses. + * + * @hide + */ + public final int readExceptionCode() { + int code = readInt(); + if (code == EX_HAS_REPLY_HEADER) { + int headerSize = readInt(); + if (headerSize == 0) { + Log.e(TAG, "Unexpected zero-sized Parcel reply header."); + } else { + // Currently the only thing in the header is StrictMode stacks, + // but discussions around event/RPC tracing suggest we might + // put that here too. If so, switch on sub-header tags here. + // But for now, just parse out the StrictMode stuff. + StrictMode.readAndHandleBinderCallViolations(this); + } + // And fat response headers are currently only used when + // there are no exceptions, so return no error: + return 0; + } + return code; + } + + /** + * Throw an exception with the given message. Not intended for use + * outside the Parcel class. + * + * @param code Used to determine which exception class to throw. + * @param msg The exception message. + */ + public final void readException(int code, String msg) { + switch (code) { + case EX_SECURITY: + throw new SecurityException(msg); + case EX_BAD_PARCELABLE: + throw new BadParcelableException(msg); + case EX_ILLEGAL_ARGUMENT: + throw new IllegalArgumentException(msg); + case EX_NULL_POINTER: + throw new NullPointerException(msg); + case EX_ILLEGAL_STATE: + throw new IllegalStateException(msg); + case EX_NETWORK_MAIN_THREAD: + throw new NetworkOnMainThreadException(); + case EX_UNSUPPORTED_OPERATION: + throw new UnsupportedOperationException(msg); + } + throw new RuntimeException("Unknown exception code: " + code + + " msg " + msg); + } + + /** + * Read an integer value from the parcel at the current dataPosition(). + */ + public final int readInt() { + return nativeReadInt(mNativePtr); + } + + /** + * Read a long integer value from the parcel at the current dataPosition(). + */ + public final long readLong() { + return nativeReadLong(mNativePtr); + } + + /** + * Read a floating point value from the parcel at the current + * dataPosition(). + */ + public final float readFloat() { + return nativeReadFloat(mNativePtr); + } + + /** + * Read a double precision floating point value from the parcel at the + * current dataPosition(). + */ + public final double readDouble() { + return nativeReadDouble(mNativePtr); + } + + /** + * Read a string value from the parcel at the current dataPosition(). + */ + public final String readString() { + return nativeReadString(mNativePtr); + } + + /** + * Read a CharSequence value from the parcel at the current dataPosition(). + * @hide + */ + public final CharSequence readCharSequence() { + return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(this); + } + + /** + * Read an object from the parcel at the current dataPosition(). + */ + public final IBinder readStrongBinder() { + return nativeReadStrongBinder(mNativePtr); + } + + /** + * Read a FileDescriptor from the parcel at the current dataPosition(). + */ + public final ParcelFileDescriptor readFileDescriptor() { + FileDescriptor fd = nativeReadFileDescriptor(mNativePtr); + return fd != null ? new ParcelFileDescriptor(fd) : null; + } + + /** {@hide} */ + public final FileDescriptor readRawFileDescriptor() { + return nativeReadFileDescriptor(mNativePtr); + } + + /*package*/ static native FileDescriptor openFileDescriptor(String file, + int mode) throws FileNotFoundException; + /*package*/ static native FileDescriptor dupFileDescriptor(FileDescriptor orig) + throws IOException; + /*package*/ static native void closeFileDescriptor(FileDescriptor desc) + throws IOException; + /*package*/ static native void clearFileDescriptor(FileDescriptor desc); + + /** + * Read a byte value from the parcel at the current dataPosition(). + */ + public final byte readByte() { + return (byte)(readInt() & 0xff); + } + + /** + * Please use {@link #readBundle(ClassLoader)} instead (whose data must have + * been written with {@link #writeBundle}. Read into an existing Map object + * from the parcel at the current dataPosition(). + */ + public final void readMap(Map outVal, ClassLoader loader) { + int N = readInt(); + readMapInternal(outVal, N, loader); + } + + /** + * Read into an existing List object from the parcel at the current + * dataPosition(), using the given class loader to load any enclosed + * Parcelables. If it is null, the default class loader is used. + */ + public final void readList(List outVal, ClassLoader loader) { + int N = readInt(); + readListInternal(outVal, N, loader); + } + + /** + * Please use {@link #readBundle(ClassLoader)} instead (whose data must have + * been written with {@link #writeBundle}. Read and return a new HashMap + * object from the parcel at the current dataPosition(), using the given + * class loader to load any enclosed Parcelables. Returns null if + * the previously written map object was null. + */ + public final HashMap readHashMap(ClassLoader loader) + { + int N = readInt(); + if (N < 0) { + return null; + } + HashMap m = new HashMap(N); + readMapInternal(m, N, loader); + return m; + } + + /** + * Read and return a new Bundle object from the parcel at the current + * dataPosition(). Returns null if the previously written Bundle object was + * null. + */ + public final Bundle readBundle() { + return readBundle(null); + } + + /** + * Read and return a new Bundle object from the parcel at the current + * dataPosition(), using the given class loader to initialize the class + * loader of the Bundle for later retrieval of Parcelable objects. + * Returns null if the previously written Bundle object was null. + */ + public final Bundle readBundle(ClassLoader loader) { + int length = readInt(); + if (length < 0) { + if (Bundle.DEBUG) Log.d(TAG, "null bundle: length=" + length); + return null; + } + + final Bundle bundle = new Bundle(this, length); + if (loader != null) { + bundle.setClassLoader(loader); + } + return bundle; + } + + /** + * Read and return a new Bundle object from the parcel at the current + * dataPosition(). Returns null if the previously written Bundle object was + * null. + */ + public final PersistableBundle readPersistableBundle() { + return readPersistableBundle(null); + } + + /** + * Read and return a new Bundle object from the parcel at the current + * dataPosition(), using the given class loader to initialize the class + * loader of the Bundle for later retrieval of Parcelable objects. + * Returns null if the previously written Bundle object was null. + */ + public final PersistableBundle readPersistableBundle(ClassLoader loader) { + int length = readInt(); + if (length < 0) { + if (Bundle.DEBUG) Log.d(TAG, "null bundle: length=" + length); + return null; + } + + final PersistableBundle bundle = new PersistableBundle(this, length); + if (loader != null) { + bundle.setClassLoader(loader); + } + return bundle; + } + + /** + * Read a Size from the parcel at the current dataPosition(). + */ + public final Size readSize() { + final int width = readInt(); + final int height = readInt(); + return new Size(width, height); + } + + /** + * Read a SizeF from the parcel at the current dataPosition(). + */ + public final SizeF readSizeF() { + final float width = readFloat(); + final float height = readFloat(); + return new SizeF(width, height); + } + + /** + * Read and return a byte[] object from the parcel. + */ + public final byte[] createByteArray() { + return nativeCreateByteArray(mNativePtr); + } + + /** + * Read a byte[] object from the parcel and copy it into the + * given byte array. + */ + public final void readByteArray(byte[] val) { + // TODO: make this a native method to avoid the extra copy. + byte[] ba = createByteArray(); + if (ba.length == val.length) { + System.arraycopy(ba, 0, val, 0, ba.length); + } else { + throw new RuntimeException("bad array lengths"); + } + } + + /** + * Read a blob of data from the parcel and return it as a byte array. + * {@hide} + * {@SystemApi} + */ + public final byte[] readBlob() { + return nativeReadBlob(mNativePtr); + } + + /** + * Read and return a String[] object from the parcel. + * {@hide} + */ + public final String[] readStringArray() { + String[] array = null; + + int length = readInt(); + if (length >= 0) + { + array = new String[length]; + + for (int i = 0 ; i < length ; i++) + { + array[i] = readString(); + } + } + + return array; + } + + /** + * Read and return a CharSequence[] object from the parcel. + * {@hide} + */ + public final CharSequence[] readCharSequenceArray() { + CharSequence[] array = null; + + int length = readInt(); + if (length >= 0) + { + array = new CharSequence[length]; + + for (int i = 0 ; i < length ; i++) + { + array[i] = readCharSequence(); + } + } + + return array; + } + + /** + * Read and return a new ArrayList object from the parcel at the current + * dataPosition(). Returns null if the previously written list object was + * null. The given class loader will be used to load any enclosed + * Parcelables. + */ + public final ArrayList readArrayList(ClassLoader loader) { + int N = readInt(); + if (N < 0) { + return null; + } + ArrayList l = new ArrayList(N); + readListInternal(l, N, loader); + return l; + } + + /** + * Read and return a new Object array from the parcel at the current + * dataPosition(). Returns null if the previously written array was + * null. The given class loader will be used to load any enclosed + * Parcelables. + */ + public final Object[] readArray(ClassLoader loader) { + int N = readInt(); + if (N < 0) { + return null; + } + Object[] l = new Object[N]; + readArrayInternal(l, N, loader); + return l; + } + + /** + * Read and return a new SparseArray object from the parcel at the current + * dataPosition(). Returns null if the previously written list object was + * null. The given class loader will be used to load any enclosed + * Parcelables. + */ + public final SparseArray readSparseArray(ClassLoader loader) { + int N = readInt(); + if (N < 0) { + return null; + } + SparseArray sa = new SparseArray(N); + readSparseArrayInternal(sa, N, loader); + return sa; + } + + /** + * Read and return a new SparseBooleanArray object from the parcel at the current + * dataPosition(). Returns null if the previously written list object was + * null. + */ + public final SparseBooleanArray readSparseBooleanArray() { + int N = readInt(); + if (N < 0) { + return null; + } + SparseBooleanArray sa = new SparseBooleanArray(N); + readSparseBooleanArrayInternal(sa, N); + return sa; + } + + /** + * Read and return a new ArrayList containing a particular object type from + * the parcel that was written with {@link #writeTypedList} at the + * current dataPosition(). Returns null if the + * previously written list object was null. The list must have + * previously been written via {@link #writeTypedList} with the same object + * type. + * + * @return A newly created ArrayList containing objects with the same data + * as those that were previously written. + * + * @see #writeTypedList + */ + public final ArrayList createTypedArrayList(Parcelable.Creator c) { + int N = readInt(); + if (N < 0) { + return null; + } + ArrayList l = new ArrayList(N); + while (N > 0) { + if (readInt() != 0) { + l.add(c.createFromParcel(this)); + } else { + l.add(null); + } + N--; + } + return l; + } + + /** + * Read into the given List items containing a particular object type + * that were written with {@link #writeTypedList} at the + * current dataPosition(). The list must have + * previously been written via {@link #writeTypedList} with the same object + * type. + * + * @return A newly created ArrayList containing objects with the same data + * as those that were previously written. + * + * @see #writeTypedList + */ + public final void readTypedList(List list, Parcelable.Creator c) { + int M = list.size(); + int N = readInt(); + int i = 0; + for (; i < M && i < N; i++) { + if (readInt() != 0) { + list.set(i, c.createFromParcel(this)); + } else { + list.set(i, null); + } + } + for (; i createStringArrayList() { + int N = readInt(); + if (N < 0) { + return null; + } + ArrayList l = new ArrayList(N); + while (N > 0) { + l.add(readString()); + N--; + } + return l; + } + + /** + * Read and return a new ArrayList containing IBinder objects from + * the parcel that was written with {@link #writeBinderList} at the + * current dataPosition(). Returns null if the + * previously written list object was null. + * + * @return A newly created ArrayList containing strings with the same data + * as those that were previously written. + * + * @see #writeBinderList + */ + public final ArrayList createBinderArrayList() { + int N = readInt(); + if (N < 0) { + return null; + } + ArrayList l = new ArrayList(N); + while (N > 0) { + l.add(readStrongBinder()); + N--; + } + return l; + } + + /** + * Read into the given List items String objects that were written with + * {@link #writeStringList} at the current dataPosition(). + * + * @return A newly created ArrayList containing strings with the same data + * as those that were previously written. + * + * @see #writeStringList + */ + public final void readStringList(List list) { + int M = list.size(); + int N = readInt(); + int i = 0; + for (; i < M && i < N; i++) { + list.set(i, readString()); + } + for (; i list) { + int M = list.size(); + int N = readInt(); + int i = 0; + for (; i < M && i < N; i++) { + list.set(i, readStrongBinder()); + } + for (; imust have + * previously been written via {@link #writeTypedArray} with the same + * object type. + * + * @return A newly created array containing objects with the same data + * as those that were previously written. + * + * @see #writeTypedArray + */ + public final T[] createTypedArray(Parcelable.Creator c) { + int N = readInt(); + if (N < 0) { + return null; + } + T[] l = c.newArray(N); + for (int i=0; i void readTypedArray(T[] val, Parcelable.Creator c) { + int N = readInt(); + if (N == val.length) { + for (int i=0; i T[] readTypedArray(Parcelable.Creator c) { + return createTypedArray(c); + } + + /** + * Write a heterogeneous array of Parcelable objects into the Parcel. + * Each object in the array is written along with its class name, so + * that the correct class can later be instantiated. As a result, this + * has significantly more overhead than {@link #writeTypedArray}, but will + * correctly handle an array containing more than one type of object. + * + * @param value The array of objects to be written. + * @param parcelableFlags Contextual flags as per + * {@link Parcelable#writeToParcel(Parcel, int) Parcelable.writeToParcel()}. + * + * @see #writeTypedArray + */ + public final void writeParcelableArray(T[] value, + int parcelableFlags) { + if (value != null) { + int N = value.length; + writeInt(N); + for (int i=0; i T readParcelable(ClassLoader loader) { + Parcelable.Creator creator = readParcelableCreator(loader); + if (creator == null) { + return null; + } + if (creator instanceof Parcelable.ClassLoaderCreator) { + return ((Parcelable.ClassLoaderCreator)creator).createFromParcel(this, loader); + } + return creator.createFromParcel(this); + } + + /** @hide */ + public final T readCreator(Parcelable.Creator creator, + ClassLoader loader) { + if (creator instanceof Parcelable.ClassLoaderCreator) { + return ((Parcelable.ClassLoaderCreator)creator).createFromParcel(this, loader); + } + return creator.createFromParcel(this); + } + + /** @hide */ + public final Parcelable.Creator readParcelableCreator( + ClassLoader loader) { + String name = readString(); + if (name == null) { + return null; + } + Parcelable.Creator creator; + synchronized (mCreators) { + HashMap map = mCreators.get(loader); + if (map == null) { + map = new HashMap(); + mCreators.put(loader, map); + } + creator = map.get(name); + if (creator == null) { + try { + Class c = loader == null ? + Class.forName(name) : Class.forName(name, true, loader); + Field f = c.getField("CREATOR"); + creator = (Parcelable.Creator)f.get(null); + } + catch (IllegalAccessException e) { + Log.e(TAG, "Illegal access when unmarshalling: " + + name, e); + throw new BadParcelableException( + "IllegalAccessException when unmarshalling: " + name); + } + catch (ClassNotFoundException e) { + Log.e(TAG, "Class not found when unmarshalling: " + + name, e); + throw new BadParcelableException( + "ClassNotFoundException when unmarshalling: " + name); + } + catch (ClassCastException e) { + throw new BadParcelableException("Parcelable protocol requires a " + + "Parcelable.Creator object called " + + " CREATOR on class " + name); + } + catch (NoSuchFieldException e) { + throw new BadParcelableException("Parcelable protocol requires a " + + "Parcelable.Creator object called " + + " CREATOR on class " + name); + } + catch (NullPointerException e) { + throw new BadParcelableException("Parcelable protocol requires " + + "the CREATOR object to be static on class " + name); + } + if (creator == null) { + throw new BadParcelableException("Parcelable protocol requires a " + + "Parcelable.Creator object called " + + " CREATOR on class " + name); + } + + map.put(name, creator); + } + } + + return creator; + } + + /** + * Read and return a new Parcelable array from the parcel. + * The given class loader will be used to load any enclosed + * Parcelables. + * @return the Parcelable array, or null if the array is null + */ + public final Parcelable[] readParcelableArray(ClassLoader loader) { + int N = readInt(); + if (N < 0) { + return null; + } + Parcelable[] p = new Parcelable[N]; + for (int i = 0; i < N; i++) { + p[i] = (Parcelable) readParcelable(loader); + } + return p; + } + + /** + * Read and return a new Serializable object from the parcel. + * @return the Serializable object, or null if the Serializable name + * wasn't found in the parcel. + */ + public final Serializable readSerializable() { + return readSerializable(null); + } + + private final Serializable readSerializable(final ClassLoader loader) { + String name = readString(); + if (name == null) { + // For some reason we were unable to read the name of the Serializable (either there + // is nothing left in the Parcel to read, or the next value wasn't a String), so + // return null, which indicates that the name wasn't found in the parcel. + return null; + } + + byte[] serializedData = createByteArray(); + ByteArrayInputStream bais = new ByteArrayInputStream(serializedData); + try { + ObjectInputStream ois = new ObjectInputStream(bais) { + @Override + protected Class resolveClass(ObjectStreamClass osClass) + throws IOException, ClassNotFoundException { + // try the custom classloader if provided + if (loader != null) { + Class c = Class.forName(osClass.getName(), false, loader); + if (c != null) { + return c; + } + } + return super.resolveClass(osClass); + } + }; + return (Serializable) ois.readObject(); + } catch (IOException ioe) { + throw new RuntimeException("Parcelable encountered " + + "IOException reading a Serializable object (name = " + name + + ")", ioe); + } catch (ClassNotFoundException cnfe) { + throw new RuntimeException("Parcelable encountered " + + "ClassNotFoundException reading a Serializable object (name = " + + name + ")", cnfe); + } + } + + // Cache of previously looked up CREATOR.createFromParcel() methods for + // particular classes. Keys are the names of the classes, values are + // Method objects. + private static final HashMap> + mCreators = new HashMap>(); + + /** @hide for internal use only. */ + static protected final Parcel obtain(int obj) { + throw new UnsupportedOperationException(); + } + + /** @hide */ + static protected final Parcel obtain(long obj) { + final Parcel[] pool = sHolderPool; + synchronized (pool) { + Parcel p; + for (int i=0; i 0) { + Object key = readValue(loader); + Object value = readValue(loader); + outVal.put(key, value); + N--; + } + } + + /* package */ void readArrayMapInternal(ArrayMap outVal, int N, + ClassLoader loader) { + if (DEBUG_ARRAY_MAP) { + RuntimeException here = new RuntimeException("here"); + here.fillInStackTrace(); + Log.d(TAG, "Reading " + N + " ArrayMap entries", here); + } + int startPos; + while (N > 0) { + if (DEBUG_ARRAY_MAP) startPos = dataPosition(); + String key = readString(); + Object value = readValue(loader); + if (DEBUG_ARRAY_MAP) Log.d(TAG, " Read #" + (N-1) + " " + + (dataPosition()-startPos) + " bytes: key=0x" + + Integer.toHexString((key != null ? key.hashCode() : 0)) + " " + key); + outVal.append(key, value); + N--; + } + outVal.validate(); + } + + /* package */ void readArrayMapSafelyInternal(ArrayMap outVal, int N, + ClassLoader loader) { + if (DEBUG_ARRAY_MAP) { + RuntimeException here = new RuntimeException("here"); + here.fillInStackTrace(); + Log.d(TAG, "Reading safely " + N + " ArrayMap entries", here); + } + while (N > 0) { + String key = readString(); + if (DEBUG_ARRAY_MAP) Log.d(TAG, " Read safe #" + (N-1) + ": key=0x" + + (key != null ? key.hashCode() : 0) + " " + key); + Object value = readValue(loader); + outVal.put(key, value); + N--; + } + } + + /** + * @hide For testing only. + */ + public void readArrayMap(ArrayMap outVal, ClassLoader loader) { + final int N = readInt(); + if (N < 0) { + return; + } + readArrayMapInternal(outVal, N, loader); + } + + private void readListInternal(List outVal, int N, + ClassLoader loader) { + while (N > 0) { + Object value = readValue(loader); + //Log.d(TAG, "Unmarshalling value=" + value); + outVal.add(value); + N--; + } + } + + private void readArrayInternal(Object[] outVal, int N, + ClassLoader loader) { + for (int i = 0; i < N; i++) { + Object value = readValue(loader); + //Log.d(TAG, "Unmarshalling value=" + value); + outVal[i] = value; + } + } + + private void readSparseArrayInternal(SparseArray outVal, int N, + ClassLoader loader) { + while (N > 0) { + int key = readInt(); + Object value = readValue(loader); + //Log.i(TAG, "Unmarshalling key=" + key + " value=" + value); + outVal.append(key, value); + N--; + } + } + + + private void readSparseBooleanArrayInternal(SparseBooleanArray outVal, int N) { + while (N > 0) { + int key = readInt(); + boolean value = this.readByte() == 1; + //Log.i(TAG, "Unmarshalling key=" + key + " value=" + value); + outVal.append(key, value); + N--; + } + } +} diff --git a/src/main/java/android/os/ParcelArrayBenchmark.java b/src/main/java/android/os/ParcelArrayBenchmark.java new file mode 100644 index 0000000..21cfb09 --- /dev/null +++ b/src/main/java/android/os/ParcelArrayBenchmark.java @@ -0,0 +1,122 @@ +/* + * 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 android.os; + +import com.google.caliper.Param; +import com.google.caliper.SimpleBenchmark; + +public class ParcelArrayBenchmark extends SimpleBenchmark { + + @Param({ "1", "10", "100", "1000" }) + private int mSize; + + private Parcel mWriteParcel; + + private byte[] mByteArray; + private int[] mIntArray; + private long[] mLongArray; + + private Parcel mByteParcel; + private Parcel mIntParcel; + private Parcel mLongParcel; + + @Override + protected void setUp() { + mWriteParcel = Parcel.obtain(); + + mByteArray = new byte[mSize]; + mIntArray = new int[mSize]; + mLongArray = new long[mSize]; + + mByteParcel = Parcel.obtain(); + mByteParcel.writeByteArray(mByteArray); + mIntParcel = Parcel.obtain(); + mIntParcel.writeIntArray(mIntArray); + mLongParcel = Parcel.obtain(); + mLongParcel.writeLongArray(mLongArray); + } + + @Override + protected void tearDown() { + mWriteParcel.recycle(); + mWriteParcel = null; + } + + public void timeWriteByteArray(int reps) { + for (int i = 0; i < reps; i++) { + mWriteParcel.setDataPosition(0); + mWriteParcel.writeByteArray(mByteArray); + } + } + + public void timeCreateByteArray(int reps) { + for (int i = 0; i < reps; i++) { + mByteParcel.setDataPosition(0); + mByteParcel.createByteArray(); + } + } + + public void timeReadByteArray(int reps) { + for (int i = 0; i < reps; i++) { + mByteParcel.setDataPosition(0); + mByteParcel.readByteArray(mByteArray); + } + } + + public void timeWriteIntArray(int reps) { + for (int i = 0; i < reps; i++) { + mWriteParcel.setDataPosition(0); + mWriteParcel.writeIntArray(mIntArray); + } + } + + public void timeCreateIntArray(int reps) { + for (int i = 0; i < reps; i++) { + mIntParcel.setDataPosition(0); + mIntParcel.createIntArray(); + } + } + + public void timeReadIntArray(int reps) { + for (int i = 0; i < reps; i++) { + mIntParcel.setDataPosition(0); + mIntParcel.readIntArray(mIntArray); + } + } + + public void timeWriteLongArray(int reps) { + for (int i = 0; i < reps; i++) { + mWriteParcel.setDataPosition(0); + mWriteParcel.writeLongArray(mLongArray); + } + } + + public void timeCreateLongArray(int reps) { + for (int i = 0; i < reps; i++) { + mLongParcel.setDataPosition(0); + mLongParcel.createLongArray(); + } + } + + public void timeReadLongArray(int reps) { + for (int i = 0; i < reps; i++) { + mLongParcel.setDataPosition(0); + mLongParcel.readLongArray(mLongArray); + } + } + +} diff --git a/src/main/java/android/os/ParcelBenchmark.java b/src/main/java/android/os/ParcelBenchmark.java new file mode 100644 index 0000000..6a7b7c8 --- /dev/null +++ b/src/main/java/android/os/ParcelBenchmark.java @@ -0,0 +1,77 @@ +/* + * 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 android.os; + +import com.google.caliper.SimpleBenchmark; + +public class ParcelBenchmark extends SimpleBenchmark { + + private Parcel mParcel; + + @Override + protected void setUp() { + mParcel = Parcel.obtain(); + } + + @Override + protected void tearDown() { + mParcel.recycle(); + mParcel = null; + } + + public void timeWriteByte(int reps) { + final byte val = 0xF; + for (int i = 0; i < reps; i++) { + mParcel.writeByte(val); + } + } + + public void timeReadByte(int reps) { + mParcel.setDataCapacity(reps); + for (int i = 0; i < reps; i++) { + mParcel.readByte(); + } + } + + public void timeWriteInt(int reps) { + final int val = 0xF; + for (int i = 0; i < reps; i++) { + mParcel.writeInt(val); + } + } + + public void timeReadInt(int reps) { + mParcel.setDataCapacity(reps << 2); + for (int i = 0; i < reps; i++) { + mParcel.readInt(); + } + } + + public void timeWriteLong(int reps) { + final long val = 0xF; + for (int i = 0; i < reps; i++) { + mParcel.writeLong(val); + } + } + + public void timeReadLong(int reps) { + mParcel.setDataCapacity(reps << 3); + for (int i = 0; i < reps; i++) { + mParcel.readLong(); + } + } +} diff --git a/src/main/java/android/os/ParcelFileDescriptor.java b/src/main/java/android/os/ParcelFileDescriptor.java new file mode 100644 index 0000000..c6b2151 --- /dev/null +++ b/src/main/java/android/os/ParcelFileDescriptor.java @@ -0,0 +1,1038 @@ +/* + * Copyright (C) 2006 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 android.os; + +import static android.system.OsConstants.AF_UNIX; +import static android.system.OsConstants.SEEK_SET; +import static android.system.OsConstants.SOCK_STREAM; +import static android.system.OsConstants.S_ISLNK; +import static android.system.OsConstants.S_ISREG; + +import android.content.BroadcastReceiver; +import android.content.ContentProvider; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.system.StructStat; +import android.util.Log; + +import dalvik.system.CloseGuard; + +import libcore.io.IoUtils; +import libcore.io.Memory; + +import java.io.Closeable; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.DatagramSocket; +import java.net.Socket; +import java.nio.ByteOrder; + +/** + * The FileDescriptor returned by {@link Parcel#readFileDescriptor}, allowing + * you to close it when done with it. + */ +public class ParcelFileDescriptor implements Parcelable, Closeable { + private static final String TAG = "ParcelFileDescriptor"; + + private final FileDescriptor mFd; + + /** + * Optional socket used to communicate close events, status at close, and + * detect remote process crashes. + */ + private FileDescriptor mCommFd; + + /** + * Wrapped {@link ParcelFileDescriptor}, if any. Used to avoid + * double-closing {@link #mFd}. + */ + private final ParcelFileDescriptor mWrapped; + + /** + * Maximum {@link #mStatusBuf} size; longer status messages will be + * truncated. + */ + private static final int MAX_STATUS = 1024; + + /** + * Temporary buffer used by {@link #readCommStatus(FileDescriptor, byte[])}, + * allocated on-demand. + */ + private byte[] mStatusBuf; + + /** + * Status read by {@link #checkError()}, or null if not read yet. + */ + private Status mStatus; + + private volatile boolean mClosed; + + private final CloseGuard mGuard = CloseGuard.get(); + + /** + * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied and + * this file doesn't already exist, then create the file with permissions + * such that any application can read it. + * + * @deprecated Creating world-readable files is very dangerous, and likely + * to cause security holes in applications. It is strongly + * discouraged; instead, applications should use more formal + * mechanism for interactions such as {@link ContentProvider}, + * {@link BroadcastReceiver}, and {@link android.app.Service}. + * There are no guarantees that this access mode will remain on + * a file, such as when it goes through a backup and restore. + */ + @Deprecated + public static final int MODE_WORLD_READABLE = 0x00000001; + + /** + * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied and + * this file doesn't already exist, then create the file with permissions + * such that any application can write it. + * + * @deprecated Creating world-writable files is very dangerous, and likely + * to cause security holes in applications. It is strongly + * discouraged; instead, applications should use more formal + * mechanism for interactions such as {@link ContentProvider}, + * {@link BroadcastReceiver}, and {@link android.app.Service}. + * There are no guarantees that this access mode will remain on + * a file, such as when it goes through a backup and restore. + */ + @Deprecated + public static final int MODE_WORLD_WRITEABLE = 0x00000002; + + /** + * For use with {@link #open}: open the file with read-only access. + */ + public static final int MODE_READ_ONLY = 0x10000000; + + /** + * For use with {@link #open}: open the file with write-only access. + */ + public static final int MODE_WRITE_ONLY = 0x20000000; + + /** + * For use with {@link #open}: open the file with read and write access. + */ + public static final int MODE_READ_WRITE = 0x30000000; + + /** + * For use with {@link #open}: create the file if it doesn't already exist. + */ + public static final int MODE_CREATE = 0x08000000; + + /** + * For use with {@link #open}: erase contents of file when opening. + */ + public static final int MODE_TRUNCATE = 0x04000000; + + /** + * For use with {@link #open}: append to end of file while writing. + */ + public static final int MODE_APPEND = 0x02000000; + + /** + * Create a new ParcelFileDescriptor wrapped around another descriptor. By + * default all method calls are delegated to the wrapped descriptor. + */ + public ParcelFileDescriptor(ParcelFileDescriptor wrapped) { + // We keep a strong reference to the wrapped PFD, and rely on its + // finalizer to trigger CloseGuard. All calls are delegated to wrapper. + mWrapped = wrapped; + mFd = null; + mCommFd = null; + mClosed = true; + } + + /** {@hide} */ + public ParcelFileDescriptor(FileDescriptor fd) { + this(fd, null); + } + + /** {@hide} */ + public ParcelFileDescriptor(FileDescriptor fd, FileDescriptor commChannel) { + if (fd == null) { + throw new NullPointerException("FileDescriptor must not be null"); + } + mWrapped = null; + mFd = fd; + mCommFd = commChannel; + mGuard.open("close"); + } + + /** + * Create a new ParcelFileDescriptor accessing a given file. + * + * @param file The file to be opened. + * @param mode The desired access mode, must be one of + * {@link #MODE_READ_ONLY}, {@link #MODE_WRITE_ONLY}, or + * {@link #MODE_READ_WRITE}; may also be any combination of + * {@link #MODE_CREATE}, {@link #MODE_TRUNCATE}, + * {@link #MODE_WORLD_READABLE}, and + * {@link #MODE_WORLD_WRITEABLE}. + * @return a new ParcelFileDescriptor pointing to the given file. + * @throws FileNotFoundException if the given file does not exist or can not + * be opened with the requested mode. + * @see #parseMode(String) + */ + public static ParcelFileDescriptor open(File file, int mode) throws FileNotFoundException { + final FileDescriptor fd = openInternal(file, mode); + if (fd == null) return null; + + return new ParcelFileDescriptor(fd); + } + + /** + * Create a new ParcelFileDescriptor accessing a given file. + * + * @param file The file to be opened. + * @param mode The desired access mode, must be one of + * {@link #MODE_READ_ONLY}, {@link #MODE_WRITE_ONLY}, or + * {@link #MODE_READ_WRITE}; may also be any combination of + * {@link #MODE_CREATE}, {@link #MODE_TRUNCATE}, + * {@link #MODE_WORLD_READABLE}, and + * {@link #MODE_WORLD_WRITEABLE}. + * @param handler to call listener from; must not be null. + * @param listener to be invoked when the returned descriptor has been + * closed; must not be null. + * @return a new ParcelFileDescriptor pointing to the given file. + * @throws FileNotFoundException if the given file does not exist or can not + * be opened with the requested mode. + * @see #parseMode(String) + */ + public static ParcelFileDescriptor open( + File file, int mode, Handler handler, OnCloseListener listener) throws IOException { + if (handler == null) { + throw new IllegalArgumentException("Handler must not be null"); + } + if (listener == null) { + throw new IllegalArgumentException("Listener must not be null"); + } + + final FileDescriptor fd = openInternal(file, mode); + if (fd == null) return null; + + final FileDescriptor[] comm = createCommSocketPair(); + final ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd, comm[0]); + + // Kick off thread to watch for status updates + IoUtils.setBlocking(comm[1], true); + final ListenerBridge bridge = new ListenerBridge(comm[1], handler.getLooper(), listener); + bridge.start(); + + return pfd; + } + + private static FileDescriptor openInternal(File file, int mode) throws FileNotFoundException { + if ((mode & MODE_READ_WRITE) == 0) { + throw new IllegalArgumentException( + "Must specify MODE_READ_ONLY, MODE_WRITE_ONLY, or MODE_READ_WRITE"); + } + + final String path = file.getPath(); + return Parcel.openFileDescriptor(path, mode); + } + + /** + * Create a new ParcelFileDescriptor that is a dup of an existing + * FileDescriptor. This obeys standard POSIX semantics, where the + * new file descriptor shared state such as file position with the + * original file descriptor. + */ + public static ParcelFileDescriptor dup(FileDescriptor orig) throws IOException { + try { + final FileDescriptor fd = Os.dup(orig); + return new ParcelFileDescriptor(fd); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + /** + * Create a new ParcelFileDescriptor that is a dup of the existing + * FileDescriptor. This obeys standard POSIX semantics, where the + * new file descriptor shared state such as file position with the + * original file descriptor. + */ + public ParcelFileDescriptor dup() throws IOException { + if (mWrapped != null) { + return mWrapped.dup(); + } else { + return dup(getFileDescriptor()); + } + } + + /** + * Create a new ParcelFileDescriptor from a raw native fd. The new + * ParcelFileDescriptor holds a dup of the original fd passed in here, + * so you must still close that fd as well as the new ParcelFileDescriptor. + * + * @param fd The native fd that the ParcelFileDescriptor should dup. + * + * @return Returns a new ParcelFileDescriptor holding a FileDescriptor + * for a dup of the given fd. + */ + public static ParcelFileDescriptor fromFd(int fd) throws IOException { + final FileDescriptor original = new FileDescriptor(); + original.setInt$(fd); + + try { + final FileDescriptor dup = Os.dup(original); + return new ParcelFileDescriptor(dup); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + /** + * Take ownership of a raw native fd in to a new ParcelFileDescriptor. + * The returned ParcelFileDescriptor now owns the given fd, and will be + * responsible for closing it. You must not close the fd yourself. + * + * @param fd The native fd that the ParcelFileDescriptor should adopt. + * + * @return Returns a new ParcelFileDescriptor holding a FileDescriptor + * for the given fd. + */ + public static ParcelFileDescriptor adoptFd(int fd) { + final FileDescriptor fdesc = new FileDescriptor(); + fdesc.setInt$(fd); + + return new ParcelFileDescriptor(fdesc); + } + + /** + * Create a new ParcelFileDescriptor from the specified Socket. The new + * ParcelFileDescriptor holds a dup of the original FileDescriptor in + * the Socket, so you must still close the Socket as well as the new + * ParcelFileDescriptor. + * + * @param socket The Socket whose FileDescriptor is used to create + * a new ParcelFileDescriptor. + * + * @return A new ParcelFileDescriptor with the FileDescriptor of the + * specified Socket. + */ + public static ParcelFileDescriptor fromSocket(Socket socket) { + FileDescriptor fd = socket.getFileDescriptor$(); + return fd != null ? new ParcelFileDescriptor(fd) : null; + } + + /** + * Create a new ParcelFileDescriptor from the specified DatagramSocket. + * + * @param datagramSocket The DatagramSocket whose FileDescriptor is used + * to create a new ParcelFileDescriptor. + * + * @return A new ParcelFileDescriptor with the FileDescriptor of the + * specified DatagramSocket. + */ + public static ParcelFileDescriptor fromDatagramSocket(DatagramSocket datagramSocket) { + FileDescriptor fd = datagramSocket.getFileDescriptor$(); + return fd != null ? new ParcelFileDescriptor(fd) : null; + } + + /** + * Create two ParcelFileDescriptors structured as a data pipe. The first + * ParcelFileDescriptor in the returned array is the read side; the second + * is the write side. + */ + public static ParcelFileDescriptor[] createPipe() throws IOException { + try { + final FileDescriptor[] fds = Os.pipe(); + return new ParcelFileDescriptor[] { + new ParcelFileDescriptor(fds[0]), + new ParcelFileDescriptor(fds[1]) }; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + /** + * Create two ParcelFileDescriptors structured as a data pipe. The first + * ParcelFileDescriptor in the returned array is the read side; the second + * is the write side. + *

    + * The write end has the ability to deliver an error message through + * {@link #closeWithError(String)} which can be handled by the read end + * calling {@link #checkError()}, usually after detecting an EOF. + * This can also be used to detect remote crashes. + */ + public static ParcelFileDescriptor[] createReliablePipe() throws IOException { + try { + final FileDescriptor[] comm = createCommSocketPair(); + final FileDescriptor[] fds = Os.pipe(); + return new ParcelFileDescriptor[] { + new ParcelFileDescriptor(fds[0], comm[0]), + new ParcelFileDescriptor(fds[1], comm[1]) }; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + /** + * Create two ParcelFileDescriptors structured as a pair of sockets + * connected to each other. The two sockets are indistinguishable. + */ + public static ParcelFileDescriptor[] createSocketPair() throws IOException { + try { + final FileDescriptor fd0 = new FileDescriptor(); + final FileDescriptor fd1 = new FileDescriptor(); + Os.socketpair(AF_UNIX, SOCK_STREAM, 0, fd0, fd1); + return new ParcelFileDescriptor[] { + new ParcelFileDescriptor(fd0), + new ParcelFileDescriptor(fd1) }; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + /** + * Create two ParcelFileDescriptors structured as a pair of sockets + * connected to each other. The two sockets are indistinguishable. + *

    + * Both ends have the ability to deliver an error message through + * {@link #closeWithError(String)} which can be detected by the other end + * calling {@link #checkError()}, usually after detecting an EOF. + * This can also be used to detect remote crashes. + */ + public static ParcelFileDescriptor[] createReliableSocketPair() throws IOException { + try { + final FileDescriptor[] comm = createCommSocketPair(); + final FileDescriptor fd0 = new FileDescriptor(); + final FileDescriptor fd1 = new FileDescriptor(); + Os.socketpair(AF_UNIX, SOCK_STREAM, 0, fd0, fd1); + return new ParcelFileDescriptor[] { + new ParcelFileDescriptor(fd0, comm[0]), + new ParcelFileDescriptor(fd1, comm[1]) }; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + private static FileDescriptor[] createCommSocketPair() throws IOException { + try { + final FileDescriptor comm1 = new FileDescriptor(); + final FileDescriptor comm2 = new FileDescriptor(); + Os.socketpair(AF_UNIX, SOCK_STREAM, 0, comm1, comm2); + IoUtils.setBlocking(comm1, false); + IoUtils.setBlocking(comm2, false); + return new FileDescriptor[] { comm1, comm2 }; + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + /** + * @hide Please use createPipe() or ContentProvider.openPipeHelper(). + * Gets a file descriptor for a read-only copy of the given data. + * + * @param data Data to copy. + * @param name Name for the shared memory area that may back the file descriptor. + * This is purely informative and may be {@code null}. + * @return A ParcelFileDescriptor. + * @throws IOException if there is an error while creating the shared memory area. + */ + @Deprecated + public static ParcelFileDescriptor fromData(byte[] data, String name) throws IOException { + if (data == null) return null; + MemoryFile file = new MemoryFile(name, data.length); + if (data.length > 0) { + file.writeBytes(data, 0, 0, data.length); + } + file.deactivate(); + FileDescriptor fd = file.getFileDescriptor(); + return fd != null ? new ParcelFileDescriptor(fd) : null; + } + + /** + * Converts a string representing a file mode, such as "rw", into a bitmask suitable for use + * with {@link #open}. + *

    + * @param mode The string representation of the file mode. + * @return A bitmask representing the given file mode. + * @throws IllegalArgumentException if the given string does not match a known file mode. + */ + public static int parseMode(String mode) { + final int modeBits; + if ("r".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_ONLY; + } else if ("w".equals(mode) || "wt".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_TRUNCATE; + } else if ("wa".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_APPEND; + } else if ("rw".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_WRITE + | ParcelFileDescriptor.MODE_CREATE; + } else if ("rwt".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_WRITE + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_TRUNCATE; + } else { + throw new IllegalArgumentException("Bad mode '" + mode + "'"); + } + return modeBits; + } + + /** + * Retrieve the actual FileDescriptor associated with this object. + * + * @return Returns the FileDescriptor associated with this object. + */ + public FileDescriptor getFileDescriptor() { + if (mWrapped != null) { + return mWrapped.getFileDescriptor(); + } else { + return mFd; + } + } + + /** + * Return the total size of the file representing this fd, as determined by + * {@code stat()}. Returns -1 if the fd is not a file. + */ + public long getStatSize() { + if (mWrapped != null) { + return mWrapped.getStatSize(); + } else { + try { + final StructStat st = Os.fstat(mFd); + if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) { + return st.st_size; + } else { + return -1; + } + } catch (ErrnoException e) { + Log.w(TAG, "fstat() failed: " + e); + return -1; + } + } + } + + /** + * This is needed for implementing AssetFileDescriptor.AutoCloseOutputStream, + * and I really don't think we want it to be public. + * @hide + */ + public long seekTo(long pos) throws IOException { + if (mWrapped != null) { + return mWrapped.seekTo(pos); + } else { + try { + return Os.lseek(mFd, pos, SEEK_SET); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + } + + /** + * Return the native fd int for this ParcelFileDescriptor. The + * ParcelFileDescriptor still owns the fd, and it still must be closed + * through this API. + */ + public int getFd() { + if (mWrapped != null) { + return mWrapped.getFd(); + } else { + if (mClosed) { + throw new IllegalStateException("Already closed"); + } + return mFd.getInt$(); + } + } + + /** + * Return the native fd int for this ParcelFileDescriptor and detach it from + * the object here. You are now responsible for closing the fd in native + * code. + *

    + * You should not detach when the original creator of the descriptor is + * expecting a reliable signal through {@link #close()} or + * {@link #closeWithError(String)}. + * + * @see #canDetectErrors() + */ + public int detachFd() { + if (mWrapped != null) { + return mWrapped.detachFd(); + } else { + if (mClosed) { + throw new IllegalStateException("Already closed"); + } + final int fd = getFd(); + Parcel.clearFileDescriptor(mFd); + writeCommStatusAndClose(Status.DETACHED, null); + return fd; + } + } + + /** + * Close the ParcelFileDescriptor. This implementation closes the underlying + * OS resources allocated to represent this stream. + * + * @throws IOException + * If an error occurs attempting to close this ParcelFileDescriptor. + */ + @Override + public void close() throws IOException { + if (mWrapped != null) { + try { + mWrapped.close(); + } finally { + releaseResources(); + } + } else { + closeWithStatus(Status.OK, null); + } + } + + /** + * Close the ParcelFileDescriptor, informing any peer that an error occurred + * while processing. If the creator of this descriptor is not observing + * errors, it will close normally. + * + * @param msg describing the error; must not be null. + */ + public void closeWithError(String msg) throws IOException { + if (mWrapped != null) { + try { + mWrapped.closeWithError(msg); + } finally { + releaseResources(); + } + } else { + if (msg == null) { + throw new IllegalArgumentException("Message must not be null"); + } + closeWithStatus(Status.ERROR, msg); + } + } + + private void closeWithStatus(int status, String msg) { + if (mClosed) return; + mClosed = true; + mGuard.close(); + // Status MUST be sent before closing actual descriptor + writeCommStatusAndClose(status, msg); + IoUtils.closeQuietly(mFd); + releaseResources(); + } + + /** + * Called when the fd is being closed, for subclasses to release any other resources + * associated with it, such as acquired providers. + * @hide + */ + public void releaseResources() { + } + + private byte[] getOrCreateStatusBuffer() { + if (mStatusBuf == null) { + mStatusBuf = new byte[MAX_STATUS]; + } + return mStatusBuf; + } + + private void writeCommStatusAndClose(int status, String msg) { + if (mCommFd == null) { + // Not reliable, or someone already sent status + if (msg != null) { + Log.w(TAG, "Unable to inform peer: " + msg); + } + return; + } + + if (status == Status.DETACHED) { + Log.w(TAG, "Peer expected signal when closed; unable to deliver after detach"); + } + + try { + if (status == Status.SILENCE) return; + + // Since we're about to close, read off any remote status. It's + // okay to remember missing here. + mStatus = readCommStatus(mCommFd, getOrCreateStatusBuffer()); + + // Skip writing status when other end has already gone away. + if (mStatus != null) return; + + try { + final byte[] buf = getOrCreateStatusBuffer(); + int writePtr = 0; + + Memory.pokeInt(buf, writePtr, status, ByteOrder.BIG_ENDIAN); + writePtr += 4; + + if (msg != null) { + final byte[] rawMsg = msg.getBytes(); + final int len = Math.min(rawMsg.length, buf.length - writePtr); + System.arraycopy(rawMsg, 0, buf, writePtr, len); + writePtr += len; + } + + Os.write(mCommFd, buf, 0, writePtr); + } catch (ErrnoException e) { + // Reporting status is best-effort + Log.w(TAG, "Failed to report status: " + e); + } catch (InterruptedIOException e) { + // Reporting status is best-effort + Log.w(TAG, "Failed to report status: " + e); + } + + } finally { + IoUtils.closeQuietly(mCommFd); + mCommFd = null; + } + } + + private static Status readCommStatus(FileDescriptor comm, byte[] buf) { + try { + final int n = Os.read(comm, buf, 0, buf.length); + if (n == 0) { + // EOF means they're dead + return new Status(Status.DEAD); + } else { + final int status = Memory.peekInt(buf, 0, ByteOrder.BIG_ENDIAN); + if (status == Status.ERROR) { + final String msg = new String(buf, 4, n - 4); + return new Status(status, msg); + } + return new Status(status); + } + } catch (ErrnoException e) { + if (e.errno == OsConstants.EAGAIN) { + // Remote is still alive, but no status written yet + return null; + } else { + Log.d(TAG, "Failed to read status; assuming dead: " + e); + return new Status(Status.DEAD); + } + } catch (InterruptedIOException e) { + Log.d(TAG, "Failed to read status; assuming dead: " + e); + return new Status(Status.DEAD); + } + } + + /** + * Indicates if this ParcelFileDescriptor can communicate and detect remote + * errors/crashes. + * + * @see #checkError() + */ + public boolean canDetectErrors() { + if (mWrapped != null) { + return mWrapped.canDetectErrors(); + } else { + return mCommFd != null; + } + } + + /** + * Detect and throw if the other end of a pipe or socket pair encountered an + * error or crashed. This allows a reader to distinguish between a valid EOF + * and an error/crash. + *

    + * If this ParcelFileDescriptor is unable to detect remote errors, it will + * return silently. + * + * @throws IOException for normal errors. + * @throws FileDescriptorDetachedException + * if the remote side called {@link #detachFd()}. Once detached, the remote + * side is unable to communicate any errors through + * {@link #closeWithError(String)}. + * @see #canDetectErrors() + */ + public void checkError() throws IOException { + if (mWrapped != null) { + mWrapped.checkError(); + } else { + if (mStatus == null) { + if (mCommFd == null) { + Log.w(TAG, "Peer didn't provide a comm channel; unable to check for errors"); + return; + } + + // Try reading status; it might be null if nothing written yet. + // Either way, we keep comm open to write our status later. + mStatus = readCommStatus(mCommFd, getOrCreateStatusBuffer()); + } + + if (mStatus == null || mStatus.status == Status.OK) { + // No status yet, or everything is peachy! + return; + } else { + throw mStatus.asIOException(); + } + } + } + + /** + * An InputStream you can create on a ParcelFileDescriptor, which will + * take care of calling {@link ParcelFileDescriptor#close + * ParcelFileDescriptor.close()} for you when the stream is closed. + */ + public static class AutoCloseInputStream extends FileInputStream { + private final ParcelFileDescriptor mPfd; + + public AutoCloseInputStream(ParcelFileDescriptor pfd) { + super(pfd.getFileDescriptor()); + mPfd = pfd; + } + + @Override + public void close() throws IOException { + try { + mPfd.close(); + } finally { + super.close(); + } + } + } + + /** + * An OutputStream you can create on a ParcelFileDescriptor, which will + * take care of calling {@link ParcelFileDescriptor#close + * ParcelFileDescriptor.close()} for you when the stream is closed. + */ + public static class AutoCloseOutputStream extends FileOutputStream { + private final ParcelFileDescriptor mPfd; + + public AutoCloseOutputStream(ParcelFileDescriptor pfd) { + super(pfd.getFileDescriptor()); + mPfd = pfd; + } + + @Override + public void close() throws IOException { + try { + mPfd.close(); + } finally { + super.close(); + } + } + } + + @Override + public String toString() { + if (mWrapped != null) { + return mWrapped.toString(); + } else { + return "{ParcelFileDescriptor: " + mFd + "}"; + } + } + + @Override + protected void finalize() throws Throwable { + if (mWrapped != null) { + releaseResources(); + } + if (mGuard != null) { + mGuard.warnIfOpen(); + } + try { + if (!mClosed) { + closeWithStatus(Status.LEAKED, null); + } + } finally { + super.finalize(); + } + } + + @Override + public int describeContents() { + if (mWrapped != null) { + return mWrapped.describeContents(); + } else { + return Parcelable.CONTENTS_FILE_DESCRIPTOR; + } + } + + /** + * {@inheritDoc} + * If {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE} is set in flags, + * the file descriptor will be closed after a copy is written to the Parcel. + */ + @Override + public void writeToParcel(Parcel out, int flags) { + // WARNING: This must stay in sync with Parcel::readParcelFileDescriptor() + // in frameworks/native/libs/binder/Parcel.cpp + if (mWrapped != null) { + try { + mWrapped.writeToParcel(out, flags); + } finally { + releaseResources(); + } + } else { + out.writeFileDescriptor(mFd); + if (mCommFd != null) { + out.writeInt(1); + out.writeFileDescriptor(mCommFd); + } else { + out.writeInt(0); + } + if ((flags & PARCELABLE_WRITE_RETURN_VALUE) != 0 && !mClosed) { + // Not a real close, so emit no status + closeWithStatus(Status.SILENCE, null); + } + } + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public ParcelFileDescriptor createFromParcel(Parcel in) { + // WARNING: This must stay in sync with Parcel::writeParcelFileDescriptor() + // in frameworks/native/libs/binder/Parcel.cpp + final FileDescriptor fd = in.readRawFileDescriptor(); + FileDescriptor commChannel = null; + if (in.readInt() != 0) { + commChannel = in.readRawFileDescriptor(); + } + return new ParcelFileDescriptor(fd, commChannel); + } + + @Override + public ParcelFileDescriptor[] newArray(int size) { + return new ParcelFileDescriptor[size]; + } + }; + + /** + * Callback indicating that a ParcelFileDescriptor has been closed. + */ + public interface OnCloseListener { + /** + * Event indicating the ParcelFileDescriptor to which this listener was + * attached has been closed. + * + * @param e error state, or {@code null} if closed cleanly. + * If the close event was the result of + * {@link ParcelFileDescriptor#detachFd()}, this will be a + * {@link FileDescriptorDetachedException}. After detach the + * remote side may continue reading/writing to the underlying + * {@link FileDescriptor}, but they can no longer deliver + * reliable close/error events. + */ + public void onClose(IOException e); + } + + /** + * Exception that indicates that the file descriptor was detached. + */ + public static class FileDescriptorDetachedException extends IOException { + + private static final long serialVersionUID = 0xDe7ac4edFdL; + + public FileDescriptorDetachedException() { + super("Remote side is detached"); + } + } + + /** + * Internal class representing a remote status read by + * {@link ParcelFileDescriptor#readCommStatus(FileDescriptor, byte[])}. + */ + private static class Status { + /** Special value indicating remote side died. */ + public static final int DEAD = -2; + /** Special value indicating no status should be written. */ + public static final int SILENCE = -1; + + /** Remote reported that everything went better than expected. */ + public static final int OK = 0; + /** Remote reported error; length and message follow. */ + public static final int ERROR = 1; + /** Remote reported {@link #detachFd()} and went rogue. */ + public static final int DETACHED = 2; + /** Remote reported their object was finalized. */ + public static final int LEAKED = 3; + + public final int status; + public final String msg; + + public Status(int status) { + this(status, null); + } + + public Status(int status, String msg) { + this.status = status; + this.msg = msg; + } + + public IOException asIOException() { + switch (status) { + case DEAD: + return new IOException("Remote side is dead"); + case OK: + return null; + case ERROR: + return new IOException("Remote error: " + msg); + case DETACHED: + return new FileDescriptorDetachedException(); + case LEAKED: + return new IOException("Remote side was leaked"); + default: + return new IOException("Unknown status: " + status); + } + } + } + + /** + * Bridge to watch for remote status, and deliver to listener. Currently + * requires that communication socket is blocking. + */ + private static final class ListenerBridge extends Thread { + // TODO: switch to using Looper to avoid burning a thread + + private FileDescriptor mCommFd; + private final Handler mHandler; + + public ListenerBridge(FileDescriptor comm, Looper looper, final OnCloseListener listener) { + mCommFd = comm; + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + final Status s = (Status) msg.obj; + listener.onClose(s != null ? s.asIOException() : null); + } + }; + } + + @Override + public void run() { + try { + final byte[] buf = new byte[MAX_STATUS]; + final Status status = readCommStatus(mCommFd, buf); + mHandler.obtainMessage(0, status).sendToTarget(); + } finally { + IoUtils.closeQuietly(mCommFd); + mCommFd = null; + } + } + } +} diff --git a/src/main/java/android/os/ParcelFormatException.java b/src/main/java/android/os/ParcelFormatException.java new file mode 100644 index 0000000..8b6fda0 --- /dev/null +++ b/src/main/java/android/os/ParcelFormatException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2006 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 android.os; + +/** + * The contents of a Parcel (usually during unmarshalling) does not + * contain the expected data. + */ +public class ParcelFormatException extends RuntimeException { + public ParcelFormatException() { + super(); + } + + public ParcelFormatException(String reason) { + super(reason); + } +} diff --git a/src/main/java/android/os/ParcelUuid.java b/src/main/java/android/os/ParcelUuid.java new file mode 100644 index 0000000..2c68ddd --- /dev/null +++ b/src/main/java/android/os/ParcelUuid.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2009 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 android.os; + +import java.util.UUID; + +/** + * This class is a Parcelable wrapper around {@link UUID} which is an + * immutable representation of a 128-bit universally unique + * identifier. + */ +public final class ParcelUuid implements Parcelable { + + private final UUID mUuid; + + /** + * Constructor creates a ParcelUuid instance from the + * given {@link UUID}. + * + * @param uuid UUID + */ + public ParcelUuid(UUID uuid) { + mUuid = uuid; + } + + /** + * Creates a new ParcelUuid from a string representation of {@link UUID}. + * + * @param uuid + * the UUID string to parse. + * @return a ParcelUuid instance. + * @throws NullPointerException + * if {@code uuid} is {@code null}. + * @throws IllegalArgumentException + * if {@code uuid} is not formatted correctly. + */ + public static ParcelUuid fromString(String uuid) { + return new ParcelUuid(UUID.fromString(uuid)); + } + + /** + * Get the {@link UUID} represented by the ParcelUuid. + * + * @return UUID contained in the ParcelUuid. + */ + public UUID getUuid() { + return mUuid; + } + + /** + * Returns a string representation of the ParcelUuid + * For example: 0000110B-0000-1000-8000-00805F9B34FB will be the return value. + * + * @return a String instance. + */ + @Override + public String toString() { + return mUuid.toString(); + } + + + @Override + public int hashCode() { + return mUuid.hashCode(); + } + + /** + * Compares this ParcelUuid to another object for equality. If {@code object} + * is not {@code null}, is a ParcelUuid instance, and all bits are equal, then + * {@code true} is returned. + * + * @param object + * the {@code Object} to compare to. + * @return {@code true} if this ParcelUuid is equal to {@code object} + * or {@code false} if not. + */ + @Override + public boolean equals(Object object) { + if (object == null) { + return false; + } + + if (this == object) { + return true; + } + + if (!(object instanceof ParcelUuid)) { + return false; + } + + ParcelUuid that = (ParcelUuid) object; + + return (this.mUuid.equals(that.mUuid)); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public ParcelUuid createFromParcel(Parcel source) { + long mostSigBits = source.readLong(); + long leastSigBits = source.readLong(); + UUID uuid = new UUID(mostSigBits, leastSigBits); + return new ParcelUuid(uuid); + } + + public ParcelUuid[] newArray(int size) { + return new ParcelUuid[size]; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mUuid.getMostSignificantBits()); + dest.writeLong(mUuid.getLeastSignificantBits()); + } +} diff --git a/src/main/java/android/os/Parcelable.java b/src/main/java/android/os/Parcelable.java new file mode 100644 index 0000000..594fbb2 --- /dev/null +++ b/src/main/java/android/os/Parcelable.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2006 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 android.os; + +/** + * Interface for classes whose instances can be written to + * and restored from a {@link Parcel}. Classes implementing the Parcelable + * interface must also have a static field called CREATOR, which + * is an object implementing the {@link Parcelable.Creator Parcelable.Creator} + * interface. + * + *

    A typical implementation of Parcelable is:

    + * + *
    + * public class MyParcelable implements Parcelable {
    + *     private int mData;
    + *
    + *     public int describeContents() {
    + *         return 0;
    + *     }
    + *
    + *     public void writeToParcel(Parcel out, int flags) {
    + *         out.writeInt(mData);
    + *     }
    + *
    + *     public static final Parcelable.Creator<MyParcelable> CREATOR
    + *             = new Parcelable.Creator<MyParcelable>() {
    + *         public MyParcelable createFromParcel(Parcel in) {
    + *             return new MyParcelable(in);
    + *         }
    + *
    + *         public MyParcelable[] newArray(int size) {
    + *             return new MyParcelable[size];
    + *         }
    + *     };
    + *     
    + *     private MyParcelable(Parcel in) {
    + *         mData = in.readInt();
    + *     }
    + * }
    + */ +public interface Parcelable { + /** + * Flag for use with {@link #writeToParcel}: the object being written + * is a return value, that is the result of a function such as + * "Parcelable someFunction()", + * "void someFunction(out Parcelable)", or + * "void someFunction(inout Parcelable)". Some implementations + * may want to release resources at this point. + */ + public static final int PARCELABLE_WRITE_RETURN_VALUE = 0x0001; + + /** + * Bit masks for use with {@link #describeContents}: each bit represents a + * kind of object considered to have potential special significance when + * marshalled. + */ + public static final int CONTENTS_FILE_DESCRIPTOR = 0x0001; + + /** + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + * + * @return a bitmask indicating the set of special object types marshalled + * by the Parcelable. + */ + public int describeContents(); + + /** + * Flatten this object in to a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + public void writeToParcel(Parcel dest, int flags); + + /** + * Interface that must be implemented and provided as a public CREATOR + * field that generates instances of your Parcelable class from a Parcel. + */ + public interface Creator { + /** + * Create a new instance of the Parcelable class, instantiating it + * from the given Parcel whose data had previously been written by + * {@link Parcelable#writeToParcel Parcelable.writeToParcel()}. + * + * @param source The Parcel to read the object's data from. + * @return Returns a new instance of the Parcelable class. + */ + public T createFromParcel(Parcel source); + + /** + * Create a new array of the Parcelable class. + * + * @param size Size of the array. + * @return Returns an array of the Parcelable class, with every entry + * initialized to null. + */ + public T[] newArray(int size); + } + + /** + * Specialization of {@link Creator} that allows you to receive the + * ClassLoader the object is being created in. + */ + public interface ClassLoaderCreator extends Creator { + /** + * Create a new instance of the Parcelable class, instantiating it + * from the given Parcel whose data had previously been written by + * {@link Parcelable#writeToParcel Parcelable.writeToParcel()} and + * using the given ClassLoader. + * + * @param source The Parcel to read the object's data from. + * @param loader The ClassLoader that this object is being created in. + * @return Returns a new instance of the Parcelable class. + */ + public T createFromParcel(Parcel source, ClassLoader loader); + } +} diff --git a/src/main/java/android/os/ParcelableParcel.java b/src/main/java/android/os/ParcelableParcel.java new file mode 100644 index 0000000..11785f1 --- /dev/null +++ b/src/main/java/android/os/ParcelableParcel.java @@ -0,0 +1,75 @@ +/* + * 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 android.os; + +/** + * Parcelable containing a raw Parcel of data. + * @hide + */ +public class ParcelableParcel implements Parcelable { + final Parcel mParcel; + final ClassLoader mClassLoader; + + public ParcelableParcel(ClassLoader loader) { + mParcel = Parcel.obtain(); + mClassLoader = loader; + } + + public ParcelableParcel(Parcel src, ClassLoader loader) { + mParcel = Parcel.obtain(); + mClassLoader = loader; + int size = src.readInt(); + int pos = src.dataPosition(); + mParcel.appendFrom(src, src.dataPosition(), size); + src.setDataPosition(pos + size); + } + + public Parcel getParcel() { + mParcel.setDataPosition(0); + return mParcel; + } + + public ClassLoader getClassLoader() { + return mClassLoader; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mParcel.dataSize()); + dest.appendFrom(mParcel, 0, mParcel.dataSize()); + } + + public static final Parcelable.ClassLoaderCreator CREATOR + = new Parcelable.ClassLoaderCreator() { + public ParcelableParcel createFromParcel(Parcel in) { + return new ParcelableParcel(in, null); + } + + public ParcelableParcel createFromParcel(Parcel in, ClassLoader loader) { + return new ParcelableParcel(in, loader); + } + + public ParcelableParcel[] newArray(int size) { + return new ParcelableParcel[size]; + } + }; +} diff --git a/src/main/java/android/os/PatternMatcher.java b/src/main/java/android/os/PatternMatcher.java new file mode 100644 index 0000000..56dc837 --- /dev/null +++ b/src/main/java/android/os/PatternMatcher.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2008 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 android.os; + +/** + * A simple pattern matcher, which is safe to use on untrusted data: it does + * not provide full reg-exp support, only simple globbing that can not be + * used maliciously. + */ +public class PatternMatcher implements Parcelable { + /** + * Pattern type: the given pattern must exactly match the string it is + * tested against. + */ + public static final int PATTERN_LITERAL = 0; + + /** + * Pattern type: the given pattern must match the + * beginning of the string it is tested against. + */ + public static final int PATTERN_PREFIX = 1; + + /** + * Pattern type: the given pattern is interpreted with a + * simple glob syntax for matching against the string it is tested against. + * In this syntax, you can use the '*' character to match against zero or + * more occurrences of the character immediately before. If the + * character before it is '.' it will match any character. The character + * '\' can be used as an escape. This essentially provides only the '*' + * wildcard part of a normal regexp. + */ + public static final int PATTERN_SIMPLE_GLOB = 2; + + private final String mPattern; + private final int mType; + + public PatternMatcher(String pattern, int type) { + mPattern = pattern; + mType = type; + } + + public final String getPath() { + return mPattern; + } + + public final int getType() { + return mType; + } + + public boolean match(String str) { + return matchPattern(mPattern, str, mType); + } + + public String toString() { + String type = "? "; + switch (mType) { + case PATTERN_LITERAL: + type = "LITERAL: "; + break; + case PATTERN_PREFIX: + type = "PREFIX: "; + break; + case PATTERN_SIMPLE_GLOB: + type = "GLOB: "; + break; + } + return "PatternMatcher{" + type + mPattern + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mPattern); + dest.writeInt(mType); + } + + public PatternMatcher(Parcel src) { + mPattern = src.readString(); + mType = src.readInt(); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public PatternMatcher createFromParcel(Parcel source) { + return new PatternMatcher(source); + } + + public PatternMatcher[] newArray(int size) { + return new PatternMatcher[size]; + } + }; + + static boolean matchPattern(String pattern, String match, int type) { + if (match == null) return false; + if (type == PATTERN_LITERAL) { + return pattern.equals(match); + } if (type == PATTERN_PREFIX) { + return match.startsWith(pattern); + } else if (type != PATTERN_SIMPLE_GLOB) { + return false; + } + + final int NP = pattern.length(); + if (NP <= 0) { + return match.length() <= 0; + } + final int NM = match.length(); + int ip = 0, im = 0; + char nextChar = pattern.charAt(0); + while ((ip= (NP-1)) { + // at the end with a pattern match, so + // all is good without checking! + return true; + } + ip++; + nextChar = pattern.charAt(ip); + // Consume everything until the next character in the + // pattern is found. + if (nextChar == '\\') { + ip++; + nextChar = ip < NP ? pattern.charAt(ip) : 0; + } + do { + if (match.charAt(im) == nextChar) { + break; + } + im++; + } while (im < NM); + if (im == NM) { + // Whoops, the next character in the pattern didn't + // exist in the match. + return false; + } + ip++; + nextChar = ip < NP ? pattern.charAt(ip) : 0; + im++; + } else { + // Consume only characters matching the one before '*'. + do { + if (match.charAt(im) != c) { + break; + } + im++; + } while (im < NM); + ip++; + nextChar = ip < NP ? pattern.charAt(ip) : 0; + } + } else { + if (c != '.' && match.charAt(im) != c) return false; + im++; + } + } + + if (ip >= NP && im >= NM) { + // Reached the end of both strings, all is good! + return true; + } + + // One last check: we may have finished the match string, but still + // have a '.*' at the end of the pattern, which should still count + // as a match. + if (ip == NP-2 && pattern.charAt(ip) == '.' + && pattern.charAt(ip+1) == '*') { + return true; + } + + return false; + } +} diff --git a/src/main/java/android/os/PerformanceCollector.java b/src/main/java/android/os/PerformanceCollector.java new file mode 100644 index 0000000..be1cf6d --- /dev/null +++ b/src/main/java/android/os/PerformanceCollector.java @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2009 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 android.os; + + +import java.util.ArrayList; + +/** + * Collects performance data between two function calls in Bundle objects and + * outputs the results using writer of type {@link PerformanceResultsWriter}. + *

    + * {@link #beginSnapshot(String)} and {@link #endSnapshot()} functions collect + * memory usage information and measure runtime between calls to begin and end. + * These functions logically wrap around an entire test, and should be called + * with name of test as the label, e.g. EmailPerformanceTest. + *

    + * {@link #startTiming(String)} and {@link #stopTiming(String)} functions + * measure runtime between calls to start and stop. These functions logically + * wrap around a single test case or a small block of code, and should be called + * with the name of test case as the label, e.g. testSimpleSendMailSequence. + *

    + * {@link #addIteration(String)} inserts intermediate measurement point which + * can be labeled with a String, e.g. Launch email app, compose, send, etc. + *

    + * Snapshot and timing functions do not interfere with each other, and thus can + * be called in any order. The intended structure is to wrap begin/endSnapshot + * around calls to start/stopTiming, for example: + *

    + * beginSnapshot("EmailPerformanceTest"); + * startTiming("testSimpleSendSequence"); + * addIteration("Launch email app"); + * addIteration("Compose"); + * stopTiming("Send"); + * startTiming("testComplexSendSequence"); + * stopTiming(""); + * startTiming("testAddLabel"); + * stopTiming(""); + * endSnapshot(); + *

    + * Structure of results output is up to implementor of + * {@link PerformanceResultsWriter }. + * + * {@hide} Pending approval for public API. + */ +public class PerformanceCollector { + + /** + * Interface for reporting performance data. + */ + public interface PerformanceResultsWriter { + + /** + * Callback invoked as first action in + * PerformanceCollector#beginSnapshot(String) for reporting the start of + * a performance snapshot. + * + * @param label description of code block between beginSnapshot and + * PerformanceCollector#endSnapshot() + * @see PerformanceCollector#beginSnapshot(String) + */ + public void writeBeginSnapshot(String label); + + /** + * Callback invoked as last action in PerformanceCollector#endSnapshot() + * for reporting performance data collected in the snapshot. + * + * @param results memory and runtime metrics stored as key/value pairs, + * in the same structure as returned by + * PerformanceCollector#endSnapshot() + * @see PerformanceCollector#endSnapshot() + */ + public void writeEndSnapshot(Bundle results); + + /** + * Callback invoked as first action in + * PerformanceCollector#startTiming(String) for reporting the start of + * a timing measurement. + * + * @param label description of code block between startTiming and + * PerformanceCollector#stopTiming(String) + * @see PerformanceCollector#startTiming(String) + */ + public void writeStartTiming(String label); + + /** + * Callback invoked as last action in + * {@link PerformanceCollector#stopTiming(String)} for reporting the + * sequence of timings measured. + * + * @param results runtime metrics of code block between calls to + * startTiming and stopTiming, in the same structure as + * returned by PerformanceCollector#stopTiming(String) + * @see PerformanceCollector#stopTiming(String) + */ + public void writeStopTiming(Bundle results); + + /** + * Callback invoked as last action in + * {@link PerformanceCollector#addMeasurement(String, long)} for + * reporting an integer type measurement. + * + * @param label short description of the metric that was measured + * @param value long value of the measurement + */ + public void writeMeasurement(String label, long value); + + /** + * Callback invoked as last action in + * {@link PerformanceCollector#addMeasurement(String, float)} for + * reporting a float type measurement. + * + * @param label short description of the metric that was measured + * @param value float value of the measurement + */ + public void writeMeasurement(String label, float value); + + /** + * Callback invoked as last action in + * {@link PerformanceCollector#addMeasurement(String, String)} for + * reporting a string field. + * + * @param label short description of the metric that was measured + * @param value string summary of the measurement + */ + public void writeMeasurement(String label, String value); + } + + /** + * In a results Bundle, this key references a List of iteration Bundles. + */ + public static final String METRIC_KEY_ITERATIONS = "iterations"; + /** + * In an iteration Bundle, this key describes the iteration. + */ + public static final String METRIC_KEY_LABEL = "label"; + /** + * In a results Bundle, this key reports the cpu time of the code block + * under measurement. + */ + public static final String METRIC_KEY_CPU_TIME = "cpu_time"; + /** + * In a results Bundle, this key reports the execution time of the code + * block under measurement. + */ + public static final String METRIC_KEY_EXECUTION_TIME = "execution_time"; + /** + * In a snapshot Bundle, this key reports the number of received + * transactions from the binder driver before collection started. + */ + public static final String METRIC_KEY_PRE_RECEIVED_TRANSACTIONS = "pre_received_transactions"; + /** + * In a snapshot Bundle, this key reports the number of transactions sent by + * the running program before collection started. + */ + public static final String METRIC_KEY_PRE_SENT_TRANSACTIONS = "pre_sent_transactions"; + /** + * In a snapshot Bundle, this key reports the number of received + * transactions from the binder driver. + */ + public static final String METRIC_KEY_RECEIVED_TRANSACTIONS = "received_transactions"; + /** + * In a snapshot Bundle, this key reports the number of transactions sent by + * the running program. + */ + public static final String METRIC_KEY_SENT_TRANSACTIONS = "sent_transactions"; + /** + * In a snapshot Bundle, this key reports the number of garbage collection + * invocations. + */ + public static final String METRIC_KEY_GC_INVOCATION_COUNT = "gc_invocation_count"; + /** + * In a snapshot Bundle, this key reports the amount of allocated memory + * used by the running program. + */ + public static final String METRIC_KEY_JAVA_ALLOCATED = "java_allocated"; + /** + * In a snapshot Bundle, this key reports the amount of free memory + * available to the running program. + */ + public static final String METRIC_KEY_JAVA_FREE = "java_free"; + /** + * In a snapshot Bundle, this key reports the number of private dirty pages + * used by dalvik. + */ + public static final String METRIC_KEY_JAVA_PRIVATE_DIRTY = "java_private_dirty"; + /** + * In a snapshot Bundle, this key reports the proportional set size for + * dalvik. + */ + public static final String METRIC_KEY_JAVA_PSS = "java_pss"; + /** + * In a snapshot Bundle, this key reports the number of shared dirty pages + * used by dalvik. + */ + public static final String METRIC_KEY_JAVA_SHARED_DIRTY = "java_shared_dirty"; + /** + * In a snapshot Bundle, this key reports the total amount of memory + * available to the running program. + */ + public static final String METRIC_KEY_JAVA_SIZE = "java_size"; + /** + * In a snapshot Bundle, this key reports the amount of allocated memory in + * the native heap. + */ + public static final String METRIC_KEY_NATIVE_ALLOCATED = "native_allocated"; + /** + * In a snapshot Bundle, this key reports the amount of free memory in the + * native heap. + */ + public static final String METRIC_KEY_NATIVE_FREE = "native_free"; + /** + * In a snapshot Bundle, this key reports the number of private dirty pages + * used by the native heap. + */ + public static final String METRIC_KEY_NATIVE_PRIVATE_DIRTY = "native_private_dirty"; + /** + * In a snapshot Bundle, this key reports the proportional set size for the + * native heap. + */ + public static final String METRIC_KEY_NATIVE_PSS = "native_pss"; + /** + * In a snapshot Bundle, this key reports the number of shared dirty pages + * used by the native heap. + */ + public static final String METRIC_KEY_NATIVE_SHARED_DIRTY = "native_shared_dirty"; + /** + * In a snapshot Bundle, this key reports the size of the native heap. + */ + public static final String METRIC_KEY_NATIVE_SIZE = "native_size"; + /** + * In a snapshot Bundle, this key reports the number of objects allocated + * globally. + */ + public static final String METRIC_KEY_GLOBAL_ALLOC_COUNT = "global_alloc_count"; + /** + * In a snapshot Bundle, this key reports the size of all objects allocated + * globally. + */ + public static final String METRIC_KEY_GLOBAL_ALLOC_SIZE = "global_alloc_size"; + /** + * In a snapshot Bundle, this key reports the number of objects freed + * globally. + */ + public static final String METRIC_KEY_GLOBAL_FREED_COUNT = "global_freed_count"; + /** + * In a snapshot Bundle, this key reports the size of all objects freed + * globally. + */ + public static final String METRIC_KEY_GLOBAL_FREED_SIZE = "global_freed_size"; + /** + * In a snapshot Bundle, this key reports the number of private dirty pages + * used by everything else. + */ + public static final String METRIC_KEY_OTHER_PRIVATE_DIRTY = "other_private_dirty"; + /** + * In a snapshot Bundle, this key reports the proportional set size for + * everything else. + */ + public static final String METRIC_KEY_OTHER_PSS = "other_pss"; + /** + * In a snapshot Bundle, this key reports the number of shared dirty pages + * used by everything else. + */ + public static final String METRIC_KEY_OTHER_SHARED_DIRTY = "other_shared_dirty"; + + private PerformanceResultsWriter mPerfWriter; + private Bundle mPerfSnapshot; + private Bundle mPerfMeasurement; + private long mSnapshotCpuTime; + private long mSnapshotExecTime; + private long mCpuTime; + private long mExecTime; + + public PerformanceCollector() { + } + + public PerformanceCollector(PerformanceResultsWriter writer) { + setPerformanceResultsWriter(writer); + } + + public void setPerformanceResultsWriter(PerformanceResultsWriter writer) { + mPerfWriter = writer; + } + + /** + * Begin collection of memory usage information. + * + * @param label description of code block between beginSnapshot and + * endSnapshot, used to label output + */ + public void beginSnapshot(String label) { + if (mPerfWriter != null) + mPerfWriter.writeBeginSnapshot(label); + startPerformanceSnapshot(); + } + + /** + * End collection of memory usage information. Returns collected data in a + * Bundle object. + * + * @return Memory and runtime metrics stored as key/value pairs. Values are + * of type long, and keys include: + *

      + *
    • {@link #METRIC_KEY_CPU_TIME cpu_time} + *
    • {@link #METRIC_KEY_EXECUTION_TIME execution_time} + *
    • {@link #METRIC_KEY_PRE_RECEIVED_TRANSACTIONS + * pre_received_transactions} + *
    • {@link #METRIC_KEY_PRE_SENT_TRANSACTIONS + * pre_sent_transactions} + *
    • {@link #METRIC_KEY_RECEIVED_TRANSACTIONS + * received_transactions} + *
    • {@link #METRIC_KEY_SENT_TRANSACTIONS sent_transactions} + *
    • {@link #METRIC_KEY_GC_INVOCATION_COUNT gc_invocation_count} + *
    • {@link #METRIC_KEY_JAVA_ALLOCATED java_allocated} + *
    • {@link #METRIC_KEY_JAVA_FREE java_free} + *
    • {@link #METRIC_KEY_JAVA_PRIVATE_DIRTY java_private_dirty} + *
    • {@link #METRIC_KEY_JAVA_PSS java_pss} + *
    • {@link #METRIC_KEY_JAVA_SHARED_DIRTY java_shared_dirty} + *
    • {@link #METRIC_KEY_JAVA_SIZE java_size} + *
    • {@link #METRIC_KEY_NATIVE_ALLOCATED native_allocated} + *
    • {@link #METRIC_KEY_NATIVE_FREE native_free} + *
    • {@link #METRIC_KEY_NATIVE_PRIVATE_DIRTY native_private_dirty} + *
    • {@link #METRIC_KEY_NATIVE_PSS native_pss} + *
    • {@link #METRIC_KEY_NATIVE_SHARED_DIRTY native_shared_dirty} + *
    • {@link #METRIC_KEY_NATIVE_SIZE native_size} + *
    • {@link #METRIC_KEY_GLOBAL_ALLOC_COUNT global_alloc_count} + *
    • {@link #METRIC_KEY_GLOBAL_ALLOC_SIZE global_alloc_size} + *
    • {@link #METRIC_KEY_GLOBAL_FREED_COUNT global_freed_count} + *
    • {@link #METRIC_KEY_GLOBAL_FREED_SIZE global_freed_size} + *
    • {@link #METRIC_KEY_OTHER_PRIVATE_DIRTY other_private_dirty} + *
    • {@link #METRIC_KEY_OTHER_PSS other_pss} + *
    • {@link #METRIC_KEY_OTHER_SHARED_DIRTY other_shared_dirty} + *
    + */ + public Bundle endSnapshot() { + endPerformanceSnapshot(); + if (mPerfWriter != null) + mPerfWriter.writeEndSnapshot(mPerfSnapshot); + return mPerfSnapshot; + } + + /** + * Start measurement of user and cpu time. + * + * @param label description of code block between startTiming and + * stopTiming, used to label output + */ + public void startTiming(String label) { + if (mPerfWriter != null) + mPerfWriter.writeStartTiming(label); + mPerfMeasurement = new Bundle(); + mPerfMeasurement.putParcelableArrayList( + METRIC_KEY_ITERATIONS, new ArrayList()); + mExecTime = SystemClock.uptimeMillis(); + mCpuTime = Process.getElapsedCpuTime(); + } + + /** + * Add a measured segment, and start measuring the next segment. Returns + * collected data in a Bundle object. + * + * @param label description of code block between startTiming and + * addIteration, and between two calls to addIteration, used + * to label output + * @return Runtime metrics stored as key/value pairs. Values are of type + * long, and keys include: + *
      + *
    • {@link #METRIC_KEY_LABEL label} + *
    • {@link #METRIC_KEY_CPU_TIME cpu_time} + *
    • {@link #METRIC_KEY_EXECUTION_TIME execution_time} + *
    + */ + public Bundle addIteration(String label) { + mCpuTime = Process.getElapsedCpuTime() - mCpuTime; + mExecTime = SystemClock.uptimeMillis() - mExecTime; + + Bundle iteration = new Bundle(); + iteration.putString(METRIC_KEY_LABEL, label); + iteration.putLong(METRIC_KEY_EXECUTION_TIME, mExecTime); + iteration.putLong(METRIC_KEY_CPU_TIME, mCpuTime); + mPerfMeasurement.getParcelableArrayList(METRIC_KEY_ITERATIONS).add(iteration); + + mExecTime = SystemClock.uptimeMillis(); + mCpuTime = Process.getElapsedCpuTime(); + return iteration; + } + + /** + * Stop measurement of user and cpu time. + * + * @param label description of code block between addIteration or + * startTiming and stopTiming, used to label output + * @return Runtime metrics stored in a bundle, including all iterations + * between calls to startTiming and stopTiming. List of iterations + * is keyed by {@link #METRIC_KEY_ITERATIONS iterations}. + */ + public Bundle stopTiming(String label) { + addIteration(label); + if (mPerfWriter != null) + mPerfWriter.writeStopTiming(mPerfMeasurement); + return mPerfMeasurement; + } + + /** + * Add an integer type measurement to the collector. + * + * @param label short description of the metric that was measured + * @param value long value of the measurement + */ + public void addMeasurement(String label, long value) { + if (mPerfWriter != null) + mPerfWriter.writeMeasurement(label, value); + } + + /** + * Add a float type measurement to the collector. + * + * @param label short description of the metric that was measured + * @param value float value of the measurement + */ + public void addMeasurement(String label, float value) { + if (mPerfWriter != null) + mPerfWriter.writeMeasurement(label, value); + } + + /** + * Add a string field to the collector. + * + * @param label short description of the metric that was measured + * @param value string summary of the measurement + */ + public void addMeasurement(String label, String value) { + if (mPerfWriter != null) + mPerfWriter.writeMeasurement(label, value); + } + + /* + * Starts tracking memory usage, binder transactions, and real & cpu timing. + */ + private void startPerformanceSnapshot() { + // Create new snapshot + mPerfSnapshot = new Bundle(); + + // Add initial binder counts + Bundle binderCounts = getBinderCounts(); + for (String key : binderCounts.keySet()) { + mPerfSnapshot.putLong("pre_" + key, binderCounts.getLong(key)); + } + + // Force a GC and zero out the performance counters. Do this + // before reading initial CPU/wall-clock times so we don't include + // the cost of this setup in our final metrics. + startAllocCounting(); + + // Record CPU time up to this point, and start timing. Note: this + // must happen at the end of this method, otherwise the timing will + // include noise. + mSnapshotExecTime = SystemClock.uptimeMillis(); + mSnapshotCpuTime = Process.getElapsedCpuTime(); + } + + /* + * Stops tracking memory usage, binder transactions, and real & cpu timing. + * Stores collected data as type long into Bundle object for reporting. + */ + private void endPerformanceSnapshot() { + // Stop the timing. This must be done first before any other counting is + // stopped. + mSnapshotCpuTime = Process.getElapsedCpuTime() - mSnapshotCpuTime; + mSnapshotExecTime = SystemClock.uptimeMillis() - mSnapshotExecTime; + + stopAllocCounting(); + + long nativeMax = Debug.getNativeHeapSize() / 1024; + long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024; + long nativeFree = Debug.getNativeHeapFreeSize() / 1024; + + Debug.MemoryInfo memInfo = new Debug.MemoryInfo(); + Debug.getMemoryInfo(memInfo); + + Runtime runtime = Runtime.getRuntime(); + + long dalvikMax = runtime.totalMemory() / 1024; + long dalvikFree = runtime.freeMemory() / 1024; + long dalvikAllocated = dalvikMax - dalvikFree; + + // Add final binder counts + Bundle binderCounts = getBinderCounts(); + for (String key : binderCounts.keySet()) { + mPerfSnapshot.putLong(key, binderCounts.getLong(key)); + } + + // Add alloc counts + Bundle allocCounts = getAllocCounts(); + for (String key : allocCounts.keySet()) { + mPerfSnapshot.putLong(key, allocCounts.getLong(key)); + } + + mPerfSnapshot.putLong(METRIC_KEY_EXECUTION_TIME, mSnapshotExecTime); + mPerfSnapshot.putLong(METRIC_KEY_CPU_TIME, mSnapshotCpuTime); + + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_SIZE, nativeMax); + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_ALLOCATED, nativeAllocated); + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_FREE, nativeFree); + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_PSS, memInfo.nativePss); + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_PRIVATE_DIRTY, memInfo.nativePrivateDirty); + mPerfSnapshot.putLong(METRIC_KEY_NATIVE_SHARED_DIRTY, memInfo.nativeSharedDirty); + + mPerfSnapshot.putLong(METRIC_KEY_JAVA_SIZE, dalvikMax); + mPerfSnapshot.putLong(METRIC_KEY_JAVA_ALLOCATED, dalvikAllocated); + mPerfSnapshot.putLong(METRIC_KEY_JAVA_FREE, dalvikFree); + mPerfSnapshot.putLong(METRIC_KEY_JAVA_PSS, memInfo.dalvikPss); + mPerfSnapshot.putLong(METRIC_KEY_JAVA_PRIVATE_DIRTY, memInfo.dalvikPrivateDirty); + mPerfSnapshot.putLong(METRIC_KEY_JAVA_SHARED_DIRTY, memInfo.dalvikSharedDirty); + + mPerfSnapshot.putLong(METRIC_KEY_OTHER_PSS, memInfo.otherPss); + mPerfSnapshot.putLong(METRIC_KEY_OTHER_PRIVATE_DIRTY, memInfo.otherPrivateDirty); + mPerfSnapshot.putLong(METRIC_KEY_OTHER_SHARED_DIRTY, memInfo.otherSharedDirty); + } + + /* + * Starts allocation counting. This triggers a gc and resets the counts. + */ + private static void startAllocCounting() { + // Before we start trigger a GC and reset the debug counts. Run the + // finalizers and another GC before starting and stopping the alloc + // counts. This will free up any objects that were just sitting around + // waiting for their finalizers to be run. + Runtime.getRuntime().gc(); + Runtime.getRuntime().runFinalization(); + Runtime.getRuntime().gc(); + + Debug.resetAllCounts(); + + // start the counts + Debug.startAllocCounting(); + } + + /* + * Stops allocation counting. + */ + private static void stopAllocCounting() { + Runtime.getRuntime().gc(); + Runtime.getRuntime().runFinalization(); + Runtime.getRuntime().gc(); + Debug.stopAllocCounting(); + } + + /* + * Returns a bundle with the current results from the allocation counting. + */ + private static Bundle getAllocCounts() { + Bundle results = new Bundle(); + results.putLong(METRIC_KEY_GLOBAL_ALLOC_COUNT, Debug.getGlobalAllocCount()); + results.putLong(METRIC_KEY_GLOBAL_ALLOC_SIZE, Debug.getGlobalAllocSize()); + results.putLong(METRIC_KEY_GLOBAL_FREED_COUNT, Debug.getGlobalFreedCount()); + results.putLong(METRIC_KEY_GLOBAL_FREED_SIZE, Debug.getGlobalFreedSize()); + results.putLong(METRIC_KEY_GC_INVOCATION_COUNT, Debug.getGlobalGcInvocationCount()); + return results; + } + + /* + * Returns a bundle with the counts for various binder counts for this + * process. Currently the only two that are reported are the number of send + * and the number of received transactions. + */ + private static Bundle getBinderCounts() { + Bundle results = new Bundle(); + results.putLong(METRIC_KEY_SENT_TRANSACTIONS, Debug.getBinderSentTransactions()); + results.putLong(METRIC_KEY_RECEIVED_TRANSACTIONS, Debug.getBinderReceivedTransactions()); + return results; + } +} diff --git a/src/main/java/android/os/PerformanceCollectorTest.java b/src/main/java/android/os/PerformanceCollectorTest.java new file mode 100644 index 0000000..7533c84 --- /dev/null +++ b/src/main/java/android/os/PerformanceCollectorTest.java @@ -0,0 +1,525 @@ +/* + * Copyright (C) 2009 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 android.os; + +import android.os.PerformanceCollector.PerformanceResultsWriter; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Random; + +import junit.framework.TestCase; + +public class PerformanceCollectorTest extends TestCase { + + private PerformanceCollector mPerfCollector; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mPerfCollector = new PerformanceCollector(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + mPerfCollector = null; + } + + @SmallTest + public void testBeginSnapshotNoWriter() throws Exception { + mPerfCollector.beginSnapshot("testBeginSnapshotNoWriter"); + + assertTrue((Long)readPrivateField("mSnapshotCpuTime", mPerfCollector) > 0); + assertTrue((Long)readPrivateField("mSnapshotExecTime", mPerfCollector) > 0); + Bundle snapshot = (Bundle)readPrivateField("mPerfSnapshot", mPerfCollector); + assertNotNull(snapshot); + assertEquals(2, snapshot.size()); + } + + @MediumTest + public void testEndSnapshotNoWriter() throws Exception { + mPerfCollector.beginSnapshot("testEndSnapshotNoWriter"); + workForRandomLongPeriod(); + Bundle snapshot = mPerfCollector.endSnapshot(); + + verifySnapshotBundle(snapshot); + } + + @SmallTest + public void testStartTimingNoWriter() throws Exception { + mPerfCollector.startTiming("testStartTimingNoWriter"); + + assertTrue((Long)readPrivateField("mCpuTime", mPerfCollector) > 0); + assertTrue((Long)readPrivateField("mExecTime", mPerfCollector) > 0); + Bundle measurement = (Bundle)readPrivateField("mPerfMeasurement", mPerfCollector); + assertNotNull(measurement); + verifyTimingBundle(measurement, new ArrayList()); + } + + @SmallTest + public void testAddIterationNoWriter() throws Exception { + mPerfCollector.startTiming("testAddIterationNoWriter"); + workForRandomTinyPeriod(); + Bundle iteration = mPerfCollector.addIteration("timing1"); + + verifyIterationBundle(iteration, "timing1"); + } + + @SmallTest + public void testStopTimingNoWriter() throws Exception { + mPerfCollector.startTiming("testStopTimingNoWriter"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("timing2"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("timing3"); + workForRandomShortPeriod(); + Bundle timing = mPerfCollector.stopTiming("timing4"); + + ArrayList labels = new ArrayList(); + labels.add("timing2"); + labels.add("timing3"); + labels.add("timing4"); + verifyTimingBundle(timing, labels); + } + + @SmallTest + public void testBeginSnapshot() throws Exception { + MockPerformanceResultsWriter writer = new MockPerformanceResultsWriter(); + mPerfCollector.setPerformanceResultsWriter(writer); + mPerfCollector.beginSnapshot("testBeginSnapshot"); + + assertEquals("testBeginSnapshot", writer.snapshotLabel); + assertTrue((Long)readPrivateField("mSnapshotCpuTime", mPerfCollector) > 0); + assertTrue((Long)readPrivateField("mSnapshotExecTime", mPerfCollector) > 0); + Bundle snapshot = (Bundle)readPrivateField("mPerfSnapshot", mPerfCollector); + assertNotNull(snapshot); + assertEquals(2, snapshot.size()); + } + + @MediumTest + public void testEndSnapshot() throws Exception { + MockPerformanceResultsWriter writer = new MockPerformanceResultsWriter(); + mPerfCollector.setPerformanceResultsWriter(writer); + mPerfCollector.beginSnapshot("testEndSnapshot"); + workForRandomLongPeriod(); + Bundle snapshot1 = mPerfCollector.endSnapshot(); + Bundle snapshot2 = writer.snapshotResults; + + assertEqualsBundle(snapshot1, snapshot2); + verifySnapshotBundle(snapshot1); + } + + @SmallTest + public void testStartTiming() throws Exception { + MockPerformanceResultsWriter writer = new MockPerformanceResultsWriter(); + mPerfCollector.setPerformanceResultsWriter(writer); + mPerfCollector.startTiming("testStartTiming"); + + assertEquals("testStartTiming", writer.timingLabel); + assertTrue((Long)readPrivateField("mCpuTime", mPerfCollector) > 0); + assertTrue((Long)readPrivateField("mExecTime", mPerfCollector) > 0); + Bundle measurement = (Bundle)readPrivateField("mPerfMeasurement", mPerfCollector); + assertNotNull(measurement); + verifyTimingBundle(measurement, new ArrayList()); + } + + @SmallTest + public void testAddIteration() throws Exception { + mPerfCollector.startTiming("testAddIteration"); + workForRandomTinyPeriod(); + Bundle iteration = mPerfCollector.addIteration("timing5"); + + verifyIterationBundle(iteration, "timing5"); + } + + @SmallTest + public void testStopTiming() throws Exception { + mPerfCollector.startTiming("testStopTiming"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("timing6"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("timing7"); + workForRandomShortPeriod(); + Bundle timing = mPerfCollector.stopTiming("timing8"); + + ArrayList labels = new ArrayList(); + labels.add("timing6"); + labels.add("timing7"); + labels.add("timing8"); + verifyTimingBundle(timing, labels); + } + + @SmallTest + public void testAddMeasurementLong() throws Exception { + MockPerformanceResultsWriter writer = new MockPerformanceResultsWriter(); + mPerfCollector.setPerformanceResultsWriter(writer); + mPerfCollector.startTiming("testAddMeasurementLong"); + mPerfCollector.addMeasurement("testAddMeasurementLongZero", 0); + mPerfCollector.addMeasurement("testAddMeasurementLongPos", 348573); + mPerfCollector.addMeasurement("testAddMeasurementLongNeg", -19354); + mPerfCollector.stopTiming(""); + + assertEquals("testAddMeasurementLong", writer.timingLabel); + Bundle results = writer.timingResults; + assertEquals(4, results.size()); + assertTrue(results.containsKey("testAddMeasurementLongZero")); + assertEquals(0, results.getLong("testAddMeasurementLongZero")); + assertTrue(results.containsKey("testAddMeasurementLongPos")); + assertEquals(348573, results.getLong("testAddMeasurementLongPos")); + assertTrue(results.containsKey("testAddMeasurementLongNeg")); + assertEquals(-19354, results.getLong("testAddMeasurementLongNeg")); + } + + @SmallTest + public void testAddMeasurementFloat() throws Exception { + MockPerformanceResultsWriter writer = new MockPerformanceResultsWriter(); + mPerfCollector.setPerformanceResultsWriter(writer); + mPerfCollector.startTiming("testAddMeasurementFloat"); + mPerfCollector.addMeasurement("testAddMeasurementFloatZero", 0.0f); + mPerfCollector.addMeasurement("testAddMeasurementFloatPos", 348573.345f); + mPerfCollector.addMeasurement("testAddMeasurementFloatNeg", -19354.093f); + mPerfCollector.stopTiming(""); + + assertEquals("testAddMeasurementFloat", writer.timingLabel); + Bundle results = writer.timingResults; + assertEquals(4, results.size()); + assertTrue(results.containsKey("testAddMeasurementFloatZero")); + assertEquals(0.0f, results.getFloat("testAddMeasurementFloatZero")); + assertTrue(results.containsKey("testAddMeasurementFloatPos")); + assertEquals(348573.345f, results.getFloat("testAddMeasurementFloatPos")); + assertTrue(results.containsKey("testAddMeasurementFloatNeg")); + assertEquals(-19354.093f, results.getFloat("testAddMeasurementFloatNeg")); + } + + @SmallTest + public void testAddMeasurementString() throws Exception { + MockPerformanceResultsWriter writer = new MockPerformanceResultsWriter(); + mPerfCollector.setPerformanceResultsWriter(writer); + mPerfCollector.startTiming("testAddMeasurementString"); + mPerfCollector.addMeasurement("testAddMeasurementStringNull", null); + mPerfCollector.addMeasurement("testAddMeasurementStringEmpty", ""); + mPerfCollector.addMeasurement("testAddMeasurementStringNonEmpty", "Hello World"); + mPerfCollector.stopTiming(""); + + assertEquals("testAddMeasurementString", writer.timingLabel); + Bundle results = writer.timingResults; + assertEquals(4, results.size()); + assertTrue(results.containsKey("testAddMeasurementStringNull")); + assertNull(results.getString("testAddMeasurementStringNull")); + assertTrue(results.containsKey("testAddMeasurementStringEmpty")); + assertEquals("", results.getString("testAddMeasurementStringEmpty")); + assertTrue(results.containsKey("testAddMeasurementStringNonEmpty")); + assertEquals("Hello World", results.getString("testAddMeasurementStringNonEmpty")); + } + + @MediumTest + public void testSimpleSequence() throws Exception { + MockPerformanceResultsWriter writer = new MockPerformanceResultsWriter(); + mPerfCollector.setPerformanceResultsWriter(writer); + mPerfCollector.beginSnapshot("testSimpleSequence"); + mPerfCollector.startTiming("testSimpleSequenceTiming"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration1"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration2"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration3"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration4"); + workForRandomShortPeriod(); + Bundle timing = mPerfCollector.stopTiming("iteration5"); + workForRandomLongPeriod(); + Bundle snapshot1 = mPerfCollector.endSnapshot(); + Bundle snapshot2 = writer.snapshotResults; + + assertEqualsBundle(snapshot1, snapshot2); + verifySnapshotBundle(snapshot1); + + ArrayList labels = new ArrayList(); + labels.add("iteration1"); + labels.add("iteration2"); + labels.add("iteration3"); + labels.add("iteration4"); + labels.add("iteration5"); + verifyTimingBundle(timing, labels); + } + + @MediumTest + public void testLongSequence() throws Exception { + MockPerformanceResultsWriter writer = new MockPerformanceResultsWriter(); + mPerfCollector.setPerformanceResultsWriter(writer); + mPerfCollector.beginSnapshot("testLongSequence"); + mPerfCollector.startTiming("testLongSequenceTiming1"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration1"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration2"); + workForRandomShortPeriod(); + Bundle timing1 = mPerfCollector.stopTiming("iteration3"); + workForRandomLongPeriod(); + + mPerfCollector.startTiming("testLongSequenceTiming2"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration4"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration5"); + workForRandomShortPeriod(); + Bundle timing2 = mPerfCollector.stopTiming("iteration6"); + workForRandomLongPeriod(); + + mPerfCollector.startTiming("testLongSequenceTiming3"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration7"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration8"); + workForRandomShortPeriod(); + Bundle timing3 = mPerfCollector.stopTiming("iteration9"); + workForRandomLongPeriod(); + + mPerfCollector.startTiming("testLongSequenceTiming4"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration10"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration11"); + workForRandomShortPeriod(); + Bundle timing4 = mPerfCollector.stopTiming("iteration12"); + workForRandomLongPeriod(); + + mPerfCollector.startTiming("testLongSequenceTiming5"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration13"); + workForRandomTinyPeriod(); + mPerfCollector.addIteration("iteration14"); + workForRandomShortPeriod(); + Bundle timing5 = mPerfCollector.stopTiming("iteration15"); + workForRandomLongPeriod(); + Bundle snapshot1 = mPerfCollector.endSnapshot(); + Bundle snapshot2 = writer.snapshotResults; + + assertEqualsBundle(snapshot1, snapshot2); + verifySnapshotBundle(snapshot1); + + ArrayList labels1 = new ArrayList(); + labels1.add("iteration1"); + labels1.add("iteration2"); + labels1.add("iteration3"); + verifyTimingBundle(timing1, labels1); + ArrayList labels2 = new ArrayList(); + labels2.add("iteration4"); + labels2.add("iteration5"); + labels2.add("iteration6"); + verifyTimingBundle(timing2, labels2); + ArrayList labels3 = new ArrayList(); + labels3.add("iteration7"); + labels3.add("iteration8"); + labels3.add("iteration9"); + verifyTimingBundle(timing3, labels3); + ArrayList labels4 = new ArrayList(); + labels4.add("iteration10"); + labels4.add("iteration11"); + labels4.add("iteration12"); + verifyTimingBundle(timing4, labels4); + ArrayList labels5 = new ArrayList(); + labels5.add("iteration13"); + labels5.add("iteration14"); + labels5.add("iteration15"); + verifyTimingBundle(timing5, labels5); + } + + /* + * Verify that snapshotting and timing do not interfere w/ each other, + * by staggering calls to snapshot and timing functions. + */ + @MediumTest + public void testOutOfOrderSequence() { + MockPerformanceResultsWriter writer = new MockPerformanceResultsWriter(); + mPerfCollector.setPerformanceResultsWriter(writer); + mPerfCollector.startTiming("testOutOfOrderSequenceTiming"); + workForRandomShortPeriod(); + mPerfCollector.beginSnapshot("testOutOfOrderSequenceSnapshot"); + workForRandomShortPeriod(); + Bundle timing1 = mPerfCollector.stopTiming("timing1"); + workForRandomShortPeriod(); + Bundle snapshot1 = mPerfCollector.endSnapshot(); + + Bundle timing2 = writer.timingResults; + Bundle snapshot2 = writer.snapshotResults; + + assertEqualsBundle(snapshot1, snapshot2); + verifySnapshotBundle(snapshot1); + + assertEqualsBundle(timing1, timing2); + ArrayList labels = new ArrayList(); + labels.add("timing1"); + verifyTimingBundle(timing1, labels); + } + + private void workForRandomPeriod(int minDuration, int maxDuration) { + Random random = new Random(); + int period = minDuration + random.nextInt(maxDuration - minDuration); + long start = Process.getElapsedCpuTime(); + // Generate positive amount of work, so cpu time is measurable in + // milliseconds + while (Process.getElapsedCpuTime() - start < period) { + for (int i = 0, temp = 0; i < 50; i++ ) { + temp += i; + } + } + } + + private void workForRandomTinyPeriod() { + workForRandomPeriod(2, 5); + } + + private void workForRandomShortPeriod() { + workForRandomPeriod(10, 25); + } + + private void workForRandomLongPeriod() { + workForRandomPeriod(50, 100); + } + + private void verifySnapshotBundle(Bundle snapshot) { + assertTrue("At least 26 metrics collected", 26 <= snapshot.size()); + + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_CPU_TIME)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_CPU_TIME) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_EXECUTION_TIME)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_EXECUTION_TIME) > 0); + + assertTrue(snapshot.containsKey( + PerformanceCollector.METRIC_KEY_PRE_RECEIVED_TRANSACTIONS)); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_PRE_SENT_TRANSACTIONS)); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_RECEIVED_TRANSACTIONS)); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_SENT_TRANSACTIONS)); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_GC_INVOCATION_COUNT)); + + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_JAVA_ALLOCATED)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_JAVA_ALLOCATED) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_JAVA_FREE)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_JAVA_FREE) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_JAVA_PRIVATE_DIRTY)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_JAVA_PRIVATE_DIRTY) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_JAVA_PSS)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_JAVA_PSS) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_JAVA_SHARED_DIRTY)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_JAVA_SHARED_DIRTY) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_JAVA_SIZE)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_JAVA_SIZE) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_NATIVE_ALLOCATED)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_NATIVE_ALLOCATED) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_NATIVE_FREE)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_NATIVE_FREE) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_NATIVE_PRIVATE_DIRTY)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_NATIVE_PRIVATE_DIRTY) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_NATIVE_PSS)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_NATIVE_PSS) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_NATIVE_SHARED_DIRTY)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_NATIVE_SHARED_DIRTY) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_NATIVE_SIZE)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_NATIVE_SIZE) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_GLOBAL_ALLOC_COUNT)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_GLOBAL_ALLOC_COUNT) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_GLOBAL_ALLOC_SIZE)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_GLOBAL_ALLOC_SIZE) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_GLOBAL_FREED_COUNT)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_GLOBAL_FREED_COUNT) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_GLOBAL_FREED_SIZE)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_GLOBAL_FREED_SIZE) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_OTHER_PRIVATE_DIRTY)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_OTHER_PRIVATE_DIRTY) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_OTHER_PSS)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_OTHER_PSS) > 0); + assertTrue(snapshot.containsKey(PerformanceCollector.METRIC_KEY_OTHER_SHARED_DIRTY)); + assertTrue(snapshot.getLong(PerformanceCollector.METRIC_KEY_OTHER_SHARED_DIRTY) > 0); + } + + private void verifyIterationBundle(Bundle iteration, String label) { + assertEquals(3, iteration.size()); + assertTrue(iteration.containsKey(PerformanceCollector.METRIC_KEY_LABEL)); + assertEquals(label, iteration.getString(PerformanceCollector.METRIC_KEY_LABEL)); + assertTrue(iteration.containsKey(PerformanceCollector.METRIC_KEY_CPU_TIME)); + assertTrue(iteration.getLong(PerformanceCollector.METRIC_KEY_CPU_TIME) > 0); + assertTrue(iteration.containsKey(PerformanceCollector.METRIC_KEY_EXECUTION_TIME)); + assertTrue(iteration.getLong(PerformanceCollector.METRIC_KEY_EXECUTION_TIME) > 0); + } + + private void verifyTimingBundle(Bundle timing, ArrayList labels) { + assertEquals(1, timing.size()); + assertTrue(timing.containsKey(PerformanceCollector.METRIC_KEY_ITERATIONS)); + ArrayList iterations = timing.getParcelableArrayList( + PerformanceCollector.METRIC_KEY_ITERATIONS); + assertNotNull(iterations); + assertEquals(labels.size(), iterations.size()); + for (int i = 0; i < labels.size(); i ++) { + Bundle iteration = (Bundle)iterations.get(i); + verifyIterationBundle(iteration, labels.get(i)); + } + } + + private void assertEqualsBundle(Bundle b1, Bundle b2) { + assertEquals(b1.keySet(), b2.keySet()); + for (String key : b1.keySet()) { + assertEquals(b1.get(key), b2.get(key)); + } + } + + private Object readPrivateField(String fieldName, Object object) throws Exception { + Field f = object.getClass().getDeclaredField(fieldName); + f.setAccessible(true); + return f.get(object); + } + + private class MockPerformanceResultsWriter implements PerformanceResultsWriter { + + public String snapshotLabel; + public Bundle snapshotResults = new Bundle(); + public String timingLabel; + public Bundle timingResults = new Bundle(); + + public void writeBeginSnapshot(String label) { + snapshotLabel = label; + } + + public void writeEndSnapshot(Bundle results) { + snapshotResults.putAll(results); + } + + public void writeStartTiming(String label) { + timingLabel = label; + } + + public void writeStopTiming(Bundle results) { + timingResults.putAll(results); + } + + public void writeMeasurement(String label, long value) { + timingResults.putLong(label, value); + } + + public void writeMeasurement(String label, float value) { + timingResults.putFloat(label, value); + } + + public void writeMeasurement(String label, String value) { + timingResults.putString(label, value); + } + } +} diff --git a/src/main/java/android/os/PersistableBundle.java b/src/main/java/android/os/PersistableBundle.java new file mode 100644 index 0000000..3a44428 --- /dev/null +++ b/src/main/java/android/os/PersistableBundle.java @@ -0,0 +1,263 @@ +/* + * 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 android.os; + +import android.util.ArrayMap; +import com.android.internal.util.XmlUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * A mapping from String values to various types that can be saved to persistent and later + * restored. + * + */ +public final class PersistableBundle extends BaseBundle implements Cloneable, Parcelable, + XmlUtils.WriteMapCallback { + private static final String TAG_PERSISTABLEMAP = "pbundle_as_map"; + public static final PersistableBundle EMPTY; + static final Parcel EMPTY_PARCEL; + + static { + EMPTY = new PersistableBundle(); + EMPTY.mMap = ArrayMap.EMPTY; + EMPTY_PARCEL = BaseBundle.EMPTY_PARCEL; + } + + /** + * Constructs a new, empty PersistableBundle. + */ + public PersistableBundle() { + super(); + } + + /** + * Constructs a new, empty PersistableBundle sized to hold the given number of + * elements. The PersistableBundle will grow as needed. + * + * @param capacity the initial capacity of the PersistableBundle + */ + public PersistableBundle(int capacity) { + super(capacity); + } + + /** + * Constructs a PersistableBundle containing a copy of the mappings from the given + * PersistableBundle. + * + * @param b a PersistableBundle to be copied. + */ + public PersistableBundle(PersistableBundle b) { + super(b); + } + + /** + * Constructs a PersistableBundle containing the mappings passed in. + * + * @param map a Map containing only those items that can be persisted. + * @throws IllegalArgumentException if any element of #map cannot be persisted. + */ + private PersistableBundle(Map map) { + super(); + + // First stuff everything in. + putAll(map); + + // Now verify each item throwing an exception if there is a violation. + Set keys = map.keySet(); + Iterator iterator = keys.iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = map.get(key); + if (value instanceof Map) { + // Fix up any Maps by replacing them with PersistableBundles. + putPersistableBundle(key, new PersistableBundle((Map) value)); + } else if (!(value instanceof Integer) && !(value instanceof Long) && + !(value instanceof Double) && !(value instanceof String) && + !(value instanceof int[]) && !(value instanceof long[]) && + !(value instanceof double[]) && !(value instanceof String[]) && + !(value instanceof PersistableBundle) && (value != null) && + !(value instanceof Boolean) && !(value instanceof boolean[])) { + throw new IllegalArgumentException("Bad value in PersistableBundle key=" + key + + " value=" + value); + } + } + } + + /* package */ PersistableBundle(Parcel parcelledData, int length) { + super(parcelledData, length); + } + + /** + * Make a PersistableBundle for a single key/value pair. + * + * @hide + */ + public static PersistableBundle forPair(String key, String value) { + PersistableBundle b = new PersistableBundle(1); + b.putString(key, value); + return b; + } + + /** + * Clones the current PersistableBundle. The internal map is cloned, but the keys and + * values to which it refers are copied by reference. + */ + @Override + public Object clone() { + return new PersistableBundle(this); + } + + /** + * Inserts a PersistableBundle value into the mapping of this Bundle, replacing + * any existing value for the given key. Either key or value may be null. + * + * @param key a String, or null + * @param value a Bundle object, or null + */ + public void putPersistableBundle(String key, PersistableBundle value) { + unparcel(); + mMap.put(key, value); + } + + /** + * Returns the value associated with the given key, or null if + * no mapping of the desired type exists for the given key or a null + * value is explicitly associated with the key. + * + * @param key a String, or null + * @return a Bundle value, or null + */ + public PersistableBundle getPersistableBundle(String key) { + unparcel(); + Object o = mMap.get(key); + if (o == null) { + return null; + } + try { + return (PersistableBundle) o; + } catch (ClassCastException e) { + typeWarning(key, o, "Bundle", e); + return null; + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public PersistableBundle createFromParcel(Parcel in) { + return in.readPersistableBundle(); + } + + @Override + public PersistableBundle[] newArray(int size) { + return new PersistableBundle[size]; + } + }; + + /** @hide */ + @Override + public void writeUnknownObject(Object v, String name, XmlSerializer out) + throws XmlPullParserException, IOException { + if (v instanceof PersistableBundle) { + out.startTag(null, TAG_PERSISTABLEMAP); + out.attribute(null, "name", name); + ((PersistableBundle) v).saveToXml(out); + out.endTag(null, TAG_PERSISTABLEMAP); + } else { + throw new XmlPullParserException("Unknown Object o=" + v); + } + } + + /** @hide */ + public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException { + unparcel(); + XmlUtils.writeMapXml(mMap, out, this); + } + + /** @hide */ + static class MyReadMapCallback implements XmlUtils.ReadMapCallback { + @Override + public Object readThisUnknownObjectXml(XmlPullParser in, String tag) + throws XmlPullParserException, IOException { + if (TAG_PERSISTABLEMAP.equals(tag)) { + return restoreFromXml(in); + } + throw new XmlPullParserException("Unknown tag=" + tag); + } + } + + /** + * Report the nature of this Parcelable's contents + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Writes the PersistableBundle contents to a Parcel, typically in order for + * it to be passed through an IBinder connection. + * @param parcel The parcel to copy this bundle to. + */ + @Override + public void writeToParcel(Parcel parcel, int flags) { + final boolean oldAllowFds = parcel.pushAllowFds(false); + try { + writeToParcelInner(parcel, flags); + } finally { + parcel.restoreAllowFds(oldAllowFds); + } + } + + /** @hide */ + public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException, + XmlPullParserException { + final int outerDepth = in.getDepth(); + final String startTag = in.getName(); + final String[] tagName = new String[1]; + int event; + while (((event = in.next()) != XmlPullParser.END_DOCUMENT) && + (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) { + if (event == XmlPullParser.START_TAG) { + return new PersistableBundle((Map) + XmlUtils.readThisMapXml(in, startTag, tagName, new MyReadMapCallback())); + } + } + return EMPTY; + } + + @Override + synchronized public String toString() { + if (mParcelledData != null) { + if (mParcelledData == EMPTY_PARCEL) { + return "PersistableBundle[EMPTY_PARCEL]"; + } else { + return "PersistableBundle[mParcelledData.dataSize=" + + mParcelledData.dataSize() + "]"; + } + } + return "PersistableBundle[" + mMap.toString() + "]"; + } + +} diff --git a/src/main/java/android/os/PowerManager.java b/src/main/java/android/os/PowerManager.java new file mode 100644 index 0000000..8307d9b --- /dev/null +++ b/src/main/java/android/os/PowerManager.java @@ -0,0 +1,1101 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.annotation.SdkConstant; +import android.annotation.SystemApi; +import android.content.Context; +import android.util.Log; + +/** + * This class gives you control of the power state of the device. + * + *

    + * Device battery life will be significantly affected by the use of this API. + * Do not acquire {@link WakeLock}s unless you really need them, use the minimum levels + * possible, and be sure to release them as soon as possible. + *

    + * You can obtain an instance of this class by calling + * {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}. + *

    + * The primary API you'll use is {@link #newWakeLock(int, String) newWakeLock()}. + * This will create a {@link PowerManager.WakeLock} object. You can then use methods + * on the wake lock object to control the power state of the device. + *

    + * In practice it's quite simple: + * {@samplecode + * PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + * PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag"); + * wl.acquire(); + * ..screen will stay on during this section.. + * wl.release(); + * } + *

    + * The following wake lock levels are defined, with varying effects on system power. + * These levels are mutually exclusive - you may only specify one of them. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Flag ValueCPU Screen Keyboard
    {@link #PARTIAL_WAKE_LOCK}On* Off Off
    {@link #SCREEN_DIM_WAKE_LOCK}On Dim Off
    {@link #SCREEN_BRIGHT_WAKE_LOCK}On Bright Off
    {@link #FULL_WAKE_LOCK}On Bright Bright
    + *

    + * *If you hold a partial wake lock, the CPU will continue to run, regardless of any + * display timeouts or the state of the screen and even after the user presses the power button. + * In all other wake locks, the CPU will run, but the user can still put the device to sleep + * using the power button. + *

    + * In addition, you can add two more flags, which affect behavior of the screen only. + * These flags have no effect when combined with a {@link #PARTIAL_WAKE_LOCK}.

    + * + * + * + * + * + * + * + * + * + * + * + *
    Flag Value Description
    {@link #ACQUIRE_CAUSES_WAKEUP}Normal wake locks don't actually turn on the illumination. Instead, they cause + * the illumination to remain on once it turns on (e.g. from user activity). This flag + * will force the screen and/or keyboard to turn on immediately, when the WakeLock is + * acquired. A typical use would be for notifications which are important for the user to + * see immediately.
    {@link #ON_AFTER_RELEASE}If this flag is set, the user activity timer will be reset when the WakeLock is + * released, causing the illumination to remain on a bit longer. This can be used to + * reduce flicker if you are cycling between wake lock conditions.
    + *

    + * Any application using a WakeLock must request the {@code android.permission.WAKE_LOCK} + * permission in an {@code <uses-permission>} element of the application's manifest. + *

    + */ +public final class PowerManager { + private static final String TAG = "PowerManager"; + + /* NOTE: Wake lock levels were previously defined as a bit field, except that only a few + * combinations were actually supported so the bit field was removed. This explains + * why the numbering scheme is so odd. If adding a new wake lock level, any unused + * value can be used. + */ + + /** + * Wake lock level: Ensures that the CPU is running; the screen and keyboard + * backlight will be allowed to go off. + *

    + * If the user presses the power button, then the screen will be turned off + * but the CPU will be kept on until all partial wake locks have been released. + *

    + */ + public static final int PARTIAL_WAKE_LOCK = 0x00000001; + + /** + * Wake lock level: Ensures that the screen is on (but may be dimmed); + * the keyboard backlight will be allowed to go off. + *

    + * If the user presses the power button, then the {@link #SCREEN_DIM_WAKE_LOCK} will be + * implicitly released by the system, causing both the screen and the CPU to be turned off. + * Contrast with {@link #PARTIAL_WAKE_LOCK}. + *

    + * + * @deprecated Most applications should use + * {@link android.view.WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON} instead + * of this type of wake lock, as it will be correctly managed by the platform + * as the user moves between applications and doesn't require a special permission. + */ + @Deprecated + public static final int SCREEN_DIM_WAKE_LOCK = 0x00000006; + + /** + * Wake lock level: Ensures that the screen is on at full brightness; + * the keyboard backlight will be allowed to go off. + *

    + * If the user presses the power button, then the {@link #SCREEN_BRIGHT_WAKE_LOCK} will be + * implicitly released by the system, causing both the screen and the CPU to be turned off. + * Contrast with {@link #PARTIAL_WAKE_LOCK}. + *

    + * + * @deprecated Most applications should use + * {@link android.view.WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON} instead + * of this type of wake lock, as it will be correctly managed by the platform + * as the user moves between applications and doesn't require a special permission. + */ + @Deprecated + public static final int SCREEN_BRIGHT_WAKE_LOCK = 0x0000000a; + + /** + * Wake lock level: Ensures that the screen and keyboard backlight are on at + * full brightness. + *

    + * If the user presses the power button, then the {@link #FULL_WAKE_LOCK} will be + * implicitly released by the system, causing both the screen and the CPU to be turned off. + * Contrast with {@link #PARTIAL_WAKE_LOCK}. + *

    + * + * @deprecated Most applications should use + * {@link android.view.WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON} instead + * of this type of wake lock, as it will be correctly managed by the platform + * as the user moves between applications and doesn't require a special permission. + */ + @Deprecated + public static final int FULL_WAKE_LOCK = 0x0000001a; + + /** + * Wake lock level: Turns the screen off when the proximity sensor activates. + *

    + * If the proximity sensor detects that an object is nearby, the screen turns off + * immediately. Shortly after the object moves away, the screen turns on again. + *

    + * A proximity wake lock does not prevent the device from falling asleep + * unlike {@link #FULL_WAKE_LOCK}, {@link #SCREEN_BRIGHT_WAKE_LOCK} and + * {@link #SCREEN_DIM_WAKE_LOCK}. If there is no user activity and no other + * wake locks are held, then the device will fall asleep (and lock) as usual. + * However, the device will not fall asleep while the screen has been turned off + * by the proximity sensor because it effectively counts as ongoing user activity. + *

    + * Since not all devices have proximity sensors, use {@link #isWakeLockLevelSupported} + * to determine whether this wake lock level is supported. + *

    + * Cannot be used with {@link #ACQUIRE_CAUSES_WAKEUP}. + *

    + */ + public static final int PROXIMITY_SCREEN_OFF_WAKE_LOCK = 0x00000020; + + /** + * Wake lock level: Put the screen in a low power state and allow the CPU to suspend + * if no other wake locks are held. + *

    + * This is used by the dream manager to implement doze mode. It currently + * has no effect unless the power manager is in the dozing state. + *

    + * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission. + *

    + * + * {@hide} + */ + public static final int DOZE_WAKE_LOCK = 0x00000040; + + /** + * Mask for the wake lock level component of a combined wake lock level and flags integer. + * + * @hide + */ + public static final int WAKE_LOCK_LEVEL_MASK = 0x0000ffff; + + /** + * Wake lock flag: Turn the screen on when the wake lock is acquired. + *

    + * Normally wake locks don't actually wake the device, they just cause + * the screen to remain on once it's already on. Think of the video player + * application as the normal behavior. Notifications that pop up and want + * the device to be on are the exception; use this flag to be like them. + *

    + * Cannot be used with {@link #PARTIAL_WAKE_LOCK}. + *

    + */ + public static final int ACQUIRE_CAUSES_WAKEUP = 0x10000000; + + /** + * Wake lock flag: When this wake lock is released, poke the user activity timer + * so the screen stays on for a little longer. + *

    + * Will not turn the screen on if it is not already on. + * See {@link #ACQUIRE_CAUSES_WAKEUP} if you want that. + *

    + * Cannot be used with {@link #PARTIAL_WAKE_LOCK}. + *

    + */ + public static final int ON_AFTER_RELEASE = 0x20000000; + + /** + * Wake lock flag: This wake lock is not important for logging events. If a later + * wake lock is acquired that is important, it will be considered the one to log. + * @hide + */ + public static final int UNIMPORTANT_FOR_LOGGING = 0x40000000; + + /** + * Flag for {@link WakeLock#release WakeLock.release(int)}: Defer releasing a + * {@link #PROXIMITY_SCREEN_OFF_WAKE_LOCK} wake lock until the proximity sensor + * indicates that an object is not in close proximity. + */ + public static final int RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY = 1; + + /** + * Brightness value for fully on. + * @hide + */ + public static final int BRIGHTNESS_ON = 255; + + /** + * Brightness value for fully off. + * @hide + */ + public static final int BRIGHTNESS_OFF = 0; + + /** + * Brightness value for default policy handling by the system. + * @hide + */ + public static final int BRIGHTNESS_DEFAULT = -1; + + // Note: Be sure to update android.os.BatteryStats and PowerManager.h + // if adding or modifying user activity event constants. + + /** + * User activity event type: Unspecified event type. + * @hide + */ + @SystemApi + public static final int USER_ACTIVITY_EVENT_OTHER = 0; + + /** + * User activity event type: Button or key pressed or released. + * @hide + */ + @SystemApi + public static final int USER_ACTIVITY_EVENT_BUTTON = 1; + + /** + * User activity event type: Touch down, move or up. + * @hide + */ + @SystemApi + public static final int USER_ACTIVITY_EVENT_TOUCH = 2; + + /** + * User activity flag: If already dimmed, extend the dim timeout + * but do not brighten. This flag is useful for keeping the screen on + * a little longer without causing a visible change such as when + * the power key is pressed. + * @hide + */ + @SystemApi + public static final int USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS = 1 << 0; + + /** + * User activity flag: Note the user activity as usual but do not + * reset the user activity timeout. This flag is useful for applying + * user activity power hints when interacting with the device indirectly + * on a secondary screen while allowing the primary screen to go to sleep. + * @hide + */ + @SystemApi + public static final int USER_ACTIVITY_FLAG_INDIRECT = 1 << 1; + + /** + * Go to sleep reason code: Going to sleep due by application request. + * @hide + */ + public static final int GO_TO_SLEEP_REASON_APPLICATION = 0; + + /** + * Go to sleep reason code: Going to sleep due by request of the + * device administration policy. + * @hide + */ + public static final int GO_TO_SLEEP_REASON_DEVICE_ADMIN = 1; + + /** + * Go to sleep reason code: Going to sleep due to a screen timeout. + * @hide + */ + public static final int GO_TO_SLEEP_REASON_TIMEOUT = 2; + + /** + * Go to sleep reason code: Going to sleep due to the lid switch being closed. + * @hide + */ + public static final int GO_TO_SLEEP_REASON_LID_SWITCH = 3; + + /** + * Go to sleep reason code: Going to sleep due to the power button being pressed. + * @hide + */ + public static final int GO_TO_SLEEP_REASON_POWER_BUTTON = 4; + + /** + * Go to sleep reason code: Going to sleep due to HDMI. + * @hide + */ + public static final int GO_TO_SLEEP_REASON_HDMI = 5; + + /** + * Go to sleep flag: Skip dozing state and directly go to full sleep. + * @hide + */ + public static final int GO_TO_SLEEP_FLAG_NO_DOZE = 1 << 0; + + /** + * The value to pass as the 'reason' argument to reboot() to + * reboot into recovery mode (for applying system updates, doing + * factory resets, etc.). + *

    + * Requires the {@link android.Manifest.permission#RECOVERY} + * permission (in addition to + * {@link android.Manifest.permission#REBOOT}). + *

    + * @hide + */ + public static final String REBOOT_RECOVERY = "recovery"; + + final Context mContext; + final IPowerManager mService; + final Handler mHandler; + + /** + * {@hide} + */ + public PowerManager(Context context, IPowerManager service, Handler handler) { + mContext = context; + mService = service; + mHandler = handler; + } + + /** + * Gets the minimum supported screen brightness setting. + * The screen may be allowed to become dimmer than this value but + * this is the minimum value that can be set by the user. + * @hide + */ + public int getMinimumScreenBrightnessSetting() { + return mContext.getResources().getInteger( + com.android.internal.R.integer.config_screenBrightnessSettingMinimum); + } + + /** + * Gets the maximum supported screen brightness setting. + * The screen may be allowed to become dimmer than this value but + * this is the maximum value that can be set by the user. + * @hide + */ + public int getMaximumScreenBrightnessSetting() { + return mContext.getResources().getInteger( + com.android.internal.R.integer.config_screenBrightnessSettingMaximum); + } + + /** + * Gets the default screen brightness setting. + * @hide + */ + public int getDefaultScreenBrightnessSetting() { + return mContext.getResources().getInteger( + com.android.internal.R.integer.config_screenBrightnessSettingDefault); + } + + /** + * Returns true if the twilight service should be used to adjust screen brightness + * policy. This setting is experimental and disabled by default. + * @hide + */ + public static boolean useTwilightAdjustmentFeature() { + return SystemProperties.getBoolean("persist.power.usetwilightadj", false); + } + + /** + * Creates a new wake lock with the specified level and flags. + *

    + * The {@code levelAndFlags} parameter specifies a wake lock level and optional flags + * combined using the logical OR operator. + *

    + * The wake lock levels are: {@link #PARTIAL_WAKE_LOCK}, + * {@link #FULL_WAKE_LOCK}, {@link #SCREEN_DIM_WAKE_LOCK} + * and {@link #SCREEN_BRIGHT_WAKE_LOCK}. Exactly one wake lock level must be + * specified as part of the {@code levelAndFlags} parameter. + *

    + * The wake lock flags are: {@link #ACQUIRE_CAUSES_WAKEUP} + * and {@link #ON_AFTER_RELEASE}. Multiple flags can be combined as part of the + * {@code levelAndFlags} parameters. + *

    + * Call {@link WakeLock#acquire() acquire()} on the object to acquire the + * wake lock, and {@link WakeLock#release release()} when you are done. + *

    + * {@samplecode + * PowerManager pm = (PowerManager)mContext.getSystemService( + * Context.POWER_SERVICE); + * PowerManager.WakeLock wl = pm.newWakeLock( + * PowerManager.SCREEN_DIM_WAKE_LOCK + * | PowerManager.ON_AFTER_RELEASE, + * TAG); + * wl.acquire(); + * // ... do work... + * wl.release(); + * } + *

    + * Although a wake lock can be created without special permissions, + * the {@link android.Manifest.permission#WAKE_LOCK} permission is + * required to actually acquire or release the wake lock that is returned. + *

    + * If using this to keep the screen on, you should strongly consider using + * {@link android.view.WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON} instead. + * This window flag will be correctly managed by the platform + * as the user moves between applications and doesn't require a special permission. + *

    + * + * @param levelAndFlags Combination of wake lock level and flag values defining + * the requested behavior of the WakeLock. + * @param tag Your class name (or other tag) for debugging purposes. + * + * @see WakeLock#acquire() + * @see WakeLock#release() + * @see #PARTIAL_WAKE_LOCK + * @see #FULL_WAKE_LOCK + * @see #SCREEN_DIM_WAKE_LOCK + * @see #SCREEN_BRIGHT_WAKE_LOCK + * @see #PROXIMITY_SCREEN_OFF_WAKE_LOCK + * @see #ACQUIRE_CAUSES_WAKEUP + * @see #ON_AFTER_RELEASE + */ + public WakeLock newWakeLock(int levelAndFlags, String tag) { + validateWakeLockParameters(levelAndFlags, tag); + return new WakeLock(levelAndFlags, tag, mContext.getOpPackageName()); + } + + /** @hide */ + public static void validateWakeLockParameters(int levelAndFlags, String tag) { + switch (levelAndFlags & WAKE_LOCK_LEVEL_MASK) { + case PARTIAL_WAKE_LOCK: + case SCREEN_DIM_WAKE_LOCK: + case SCREEN_BRIGHT_WAKE_LOCK: + case FULL_WAKE_LOCK: + case PROXIMITY_SCREEN_OFF_WAKE_LOCK: + case DOZE_WAKE_LOCK: + break; + default: + throw new IllegalArgumentException("Must specify a valid wake lock level."); + } + if (tag == null) { + throw new IllegalArgumentException("The tag must not be null."); + } + } + + /** + * Notifies the power manager that user activity happened. + *

    + * Resets the auto-off timer and brightens the screen if the device + * is not asleep. This is what happens normally when a key or the touch + * screen is pressed or when some other user activity occurs. + * This method does not wake up the device if it has been put to sleep. + *

    + * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission. + *

    + * + * @param when The time of the user activity, in the {@link SystemClock#uptimeMillis()} + * time base. This timestamp is used to correctly order the user activity request with + * other power management functions. It should be set + * to the timestamp of the input event that caused the user activity. + * @param noChangeLights If true, does not cause the keyboard backlight to turn on + * because of this event. This is set when the power key is pressed. + * We want the device to stay on while the button is down, but we're about + * to turn off the screen so we don't want the keyboard backlight to turn on again. + * Otherwise the lights flash on and then off and it looks weird. + * + * @see #wakeUp + * @see #goToSleep + * + * @removed Requires signature or system permission. + * @deprecated Use {@link #userActivity(long, int, int)}. + */ + @Deprecated + public void userActivity(long when, boolean noChangeLights) { + userActivity(when, USER_ACTIVITY_EVENT_OTHER, + noChangeLights ? USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS : 0); + } + + /** + * Notifies the power manager that user activity happened. + *

    + * Resets the auto-off timer and brightens the screen if the device + * is not asleep. This is what happens normally when a key or the touch + * screen is pressed or when some other user activity occurs. + * This method does not wake up the device if it has been put to sleep. + *

    + * Requires the {@link android.Manifest.permission#DEVICE_POWER} or + * {@link android.Manifest.permission#USER_ACTIVITY} permission. + *

    + * + * @param when The time of the user activity, in the {@link SystemClock#uptimeMillis()} + * time base. This timestamp is used to correctly order the user activity request with + * other power management functions. It should be set + * to the timestamp of the input event that caused the user activity. + * @param event The user activity event. + * @param flags Optional user activity flags. + * + * @see #wakeUp + * @see #goToSleep + * + * @hide Requires signature or system permission. + */ + @SystemApi + public void userActivity(long when, int event, int flags) { + try { + mService.userActivity(when, event, flags); + } catch (RemoteException e) { + } + } + + /** + * Forces the device to go to sleep. + *

    + * Overrides all the wake locks that are held. + * This is what happens when the power key is pressed to turn off the screen. + *

    + * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission. + *

    + * + * @param time The time when the request to go to sleep was issued, in the + * {@link SystemClock#uptimeMillis()} time base. This timestamp is used to correctly + * order the go to sleep request with other power management functions. It should be set + * to the timestamp of the input event that caused the request to go to sleep. + * + * @see #userActivity + * @see #wakeUp + * + * @removed Requires signature permission. + */ + public void goToSleep(long time) { + goToSleep(time, GO_TO_SLEEP_REASON_APPLICATION, 0); + } + + /** + * Forces the device to go to sleep. + *

    + * Overrides all the wake locks that are held. + * This is what happens when the power key is pressed to turn off the screen. + *

    + * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission. + *

    + * + * @param time The time when the request to go to sleep was issued, in the + * {@link SystemClock#uptimeMillis()} time base. This timestamp is used to correctly + * order the go to sleep request with other power management functions. It should be set + * to the timestamp of the input event that caused the request to go to sleep. + * @param reason The reason the device is going to sleep. + * @param flags Optional flags to apply when going to sleep. + * + * @see #userActivity + * @see #wakeUp + * + * @hide Requires signature permission. + */ + public void goToSleep(long time, int reason, int flags) { + try { + mService.goToSleep(time, reason, flags); + } catch (RemoteException e) { + } + } + + /** + * Forces the device to wake up from sleep. + *

    + * If the device is currently asleep, wakes it up, otherwise does nothing. + * This is what happens when the power key is pressed to turn on the screen. + *

    + * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission. + *

    + * + * @param time The time when the request to wake up was issued, in the + * {@link SystemClock#uptimeMillis()} time base. This timestamp is used to correctly + * order the wake up request with other power management functions. It should be set + * to the timestamp of the input event that caused the request to wake up. + * + * @see #userActivity + * @see #goToSleep + * + * @removed Requires signature permission. + */ + public void wakeUp(long time) { + try { + mService.wakeUp(time); + } catch (RemoteException e) { + } + } + + /** + * Forces the device to start napping. + *

    + * If the device is currently awake, starts dreaming, otherwise does nothing. + * When the dream ends or if the dream cannot be started, the device will + * either wake up or go to sleep depending on whether there has been recent + * user activity. + *

    + * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission. + *

    + * + * @param time The time when the request to nap was issued, in the + * {@link SystemClock#uptimeMillis()} time base. This timestamp is used to correctly + * order the nap request with other power management functions. It should be set + * to the timestamp of the input event that caused the request to nap. + * + * @see #wakeUp + * @see #goToSleep + * + * @hide Requires signature permission. + */ + public void nap(long time) { + try { + mService.nap(time); + } catch (RemoteException e) { + } + } + + /** + * Boosts the brightness of the screen to maximum for a predetermined + * period of time. This is used to make the screen more readable in bright + * daylight for a short duration. + *

    + * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission. + *

    + * + * @param time The time when the request to boost was issued, in the + * {@link SystemClock#uptimeMillis()} time base. This timestamp is used to correctly + * order the boost request with other power management functions. It should be set + * to the timestamp of the input event that caused the request to boost. + * + * @hide Requires signature permission. + */ + public void boostScreenBrightness(long time) { + try { + mService.boostScreenBrightness(time); + } catch (RemoteException e) { + } + } + + /** + * Sets the brightness of the backlights (screen, keyboard, button). + *

    + * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission. + *

    + * + * @param brightness The brightness value from 0 to 255. + * + * @hide Requires signature permission. + */ + public void setBacklightBrightness(int brightness) { + try { + mService.setTemporaryScreenBrightnessSettingOverride(brightness); + } catch (RemoteException e) { + } + } + + /** + * Returns true if the specified wake lock level is supported. + * + * @param level The wake lock level to check. + * @return True if the specified wake lock level is supported. + */ + public boolean isWakeLockLevelSupported(int level) { + try { + return mService.isWakeLockLevelSupported(level); + } catch (RemoteException e) { + return false; + } + } + + /** + * Returns true if the device is in an interactive state. + *

    + * For historical reasons, the name of this method refers to the power state of + * the screen but it actually describes the overall interactive state of + * the device. This method has been replaced by {@link #isInteractive}. + *

    + * The value returned by this method only indicates whether the device is + * in an interactive state which may have nothing to do with the screen being + * on or off. To determine the actual state of the screen, + * use {@link android.view.Display#getState}. + *

    + * + * @return True if the device is in an interactive state. + * + * @deprecated Use {@link #isInteractive} instead. + */ + @Deprecated + public boolean isScreenOn() { + return isInteractive(); + } + + /** + * Returns true if the device is in an interactive state. + *

    + * When this method returns true, the device is awake and ready to interact + * with the user (although this is not a guarantee that the user is actively + * interacting with the device just this moment). The main screen is usually + * turned on while in this state. Certain features, such as the proximity + * sensor, may temporarily turn off the screen while still leaving the device in an + * interactive state. Note in particular that the device is still considered + * to be interactive while dreaming (since dreams can be interactive) but not + * when it is dozing or asleep. + *

    + * When this method returns false, the device is dozing or asleep and must + * be awoken before it will become ready to interact with the user again. The + * main screen is usually turned off while in this state. Certain features, + * such as "ambient mode" may cause the main screen to remain on (albeit in a + * low power state) to display system-provided content while the device dozes. + *

    + * The system will send a {@link android.content.Intent#ACTION_SCREEN_ON screen on} + * or {@link android.content.Intent#ACTION_SCREEN_OFF screen off} broadcast + * whenever the interactive state of the device changes. For historical reasons, + * the names of these broadcasts refer to the power state of the screen + * but they are actually sent in response to changes in the overall interactive + * state of the device, as described by this method. + *

    + * Services may use the non-interactive state as a hint to conserve power + * since the user is not present. + *

    + * + * @return True if the device is in an interactive state. + * + * @see android.content.Intent#ACTION_SCREEN_ON + * @see android.content.Intent#ACTION_SCREEN_OFF + */ + public boolean isInteractive() { + try { + return mService.isInteractive(); + } catch (RemoteException e) { + return false; + } + } + + /** + * Reboot the device. Will not return if the reboot is successful. + *

    + * Requires the {@link android.Manifest.permission#REBOOT} permission. + *

    + * + * @param reason code to pass to the kernel (e.g., "recovery") to + * request special boot modes, or null. + */ + public void reboot(String reason) { + try { + mService.reboot(false, reason, true); + } catch (RemoteException e) { + } + } + + /** + * Returns true if the device is currently in power save mode. When in this mode, + * applications should reduce their functionality in order to conserve battery as + * much as possible. You can monitor for changes to this state with + * {@link #ACTION_POWER_SAVE_MODE_CHANGED}. + * + * @return Returns true if currently in low power mode, else false. + */ + public boolean isPowerSaveMode() { + try { + return mService.isPowerSaveMode(); + } catch (RemoteException e) { + return false; + } + } + + /** + * Set the current power save mode. + * + * @return True if the set was allowed. + * + * @see #isPowerSaveMode() + * + * @hide + */ + public boolean setPowerSaveMode(boolean mode) { + try { + return mService.setPowerSaveMode(mode); + } catch (RemoteException e) { + return false; + } + } + + /** + * Intent that is broadcast when the state of {@link #isPowerSaveMode()} changes. + * This broadcast is only sent to registered receivers. + */ + @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_POWER_SAVE_MODE_CHANGED + = "android.os.action.POWER_SAVE_MODE_CHANGED"; + + /** + * Intent that is broadcast when the state of {@link #isPowerSaveMode()} is about to change. + * This broadcast is only sent to registered receivers. + * + * @hide + */ + @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_POWER_SAVE_MODE_CHANGING + = "android.os.action.POWER_SAVE_MODE_CHANGING"; + + /** @hide */ + public static final String EXTRA_POWER_SAVE_MODE = "mode"; + + /** + * A wake lock is a mechanism to indicate that your application needs + * to have the device stay on. + *

    + * Any application using a WakeLock must request the {@code android.permission.WAKE_LOCK} + * permission in an {@code <uses-permission>} element of the application's manifest. + * Obtain a wake lock by calling {@link PowerManager#newWakeLock(int, String)}. + *

    + * Call {@link #acquire()} to acquire the wake lock and force the device to stay + * on at the level that was requested when the wake lock was created. + *

    + * Call {@link #release()} when you are done and don't need the lock anymore. + * It is very important to do this as soon as possible to avoid running down the + * device's battery excessively. + *

    + */ + public final class WakeLock { + private int mFlags; + private String mTag; + private final String mPackageName; + private final IBinder mToken; + private int mCount; + private boolean mRefCounted = true; + private boolean mHeld; + private WorkSource mWorkSource; + private String mHistoryTag; + private final String mTraceName; + + private final Runnable mReleaser = new Runnable() { + public void run() { + release(); + } + }; + + WakeLock(int flags, String tag, String packageName) { + mFlags = flags; + mTag = tag; + mPackageName = packageName; + mToken = new Binder(); + mTraceName = "WakeLock (" + mTag + ")"; + } + + @Override + protected void finalize() throws Throwable { + synchronized (mToken) { + if (mHeld) { + Log.wtf(TAG, "WakeLock finalized while still held: " + mTag); + Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0); + try { + mService.releaseWakeLock(mToken, 0); + } catch (RemoteException e) { + } + } + } + } + + /** + * Sets whether this WakeLock is reference counted. + *

    + * Wake locks are reference counted by default. If a wake lock is + * reference counted, then each call to {@link #acquire()} must be + * balanced by an equal number of calls to {@link #release()}. If a wake + * lock is not reference counted, then one call to {@link #release()} is + * sufficient to undo the effect of all previous calls to {@link #acquire()}. + *

    + * + * @param value True to make the wake lock reference counted, false to + * make the wake lock non-reference counted. + */ + public void setReferenceCounted(boolean value) { + synchronized (mToken) { + mRefCounted = value; + } + } + + /** + * Acquires the wake lock. + *

    + * Ensures that the device is on at the level requested when + * the wake lock was created. + *

    + */ + public void acquire() { + synchronized (mToken) { + acquireLocked(); + } + } + + /** + * Acquires the wake lock with a timeout. + *

    + * Ensures that the device is on at the level requested when + * the wake lock was created. The lock will be released after the given timeout + * expires. + *

    + * + * @param timeout The timeout after which to release the wake lock, in milliseconds. + */ + public void acquire(long timeout) { + synchronized (mToken) { + acquireLocked(); + mHandler.postDelayed(mReleaser, timeout); + } + } + + private void acquireLocked() { + if (!mRefCounted || mCount++ == 0) { + // Do this even if the wake lock is already thought to be held (mHeld == true) + // because non-reference counted wake locks are not always properly released. + // For example, the keyguard's wake lock might be forcibly released by the + // power manager without the keyguard knowing. A subsequent call to acquire + // should immediately acquire the wake lock once again despite never having + // been explicitly released by the keyguard. + mHandler.removeCallbacks(mReleaser); + Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, mTraceName, 0); + try { + mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource, + mHistoryTag); + } catch (RemoteException e) { + } + mHeld = true; + } + } + + /** + * Releases the wake lock. + *

    + * This method releases your claim to the CPU or screen being on. + * The screen may turn off shortly after you release the wake lock, or it may + * not if there are other wake locks still held. + *

    + */ + public void release() { + release(0); + } + + /** + * Releases the wake lock with flags to modify the release behavior. + *

    + * This method releases your claim to the CPU or screen being on. + * The screen may turn off shortly after you release the wake lock, or it may + * not if there are other wake locks still held. + *

    + * + * @param flags Combination of flag values to modify the release behavior. + * Currently only {@link #RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY} is supported. + * Passing 0 is equivalent to calling {@link #release()}. + */ + public void release(int flags) { + synchronized (mToken) { + if (!mRefCounted || --mCount == 0) { + mHandler.removeCallbacks(mReleaser); + if (mHeld) { + Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0); + try { + mService.releaseWakeLock(mToken, flags); + } catch (RemoteException e) { + } + mHeld = false; + } + } + if (mCount < 0) { + throw new RuntimeException("WakeLock under-locked " + mTag); + } + } + } + + /** + * Returns true if the wake lock has been acquired but not yet released. + * + * @return True if the wake lock is held. + */ + public boolean isHeld() { + synchronized (mToken) { + return mHeld; + } + } + + /** + * Sets the work source associated with the wake lock. + *

    + * The work source is used to determine on behalf of which application + * the wake lock is being held. This is useful in the case where a + * service is performing work on behalf of an application so that the + * cost of that work can be accounted to the application. + *

    + * + * @param ws The work source, or null if none. + */ + public void setWorkSource(WorkSource ws) { + synchronized (mToken) { + if (ws != null && ws.size() == 0) { + ws = null; + } + + final boolean changed; + if (ws == null) { + changed = mWorkSource != null; + mWorkSource = null; + } else if (mWorkSource == null) { + changed = true; + mWorkSource = new WorkSource(ws); + } else { + changed = mWorkSource.diff(ws); + if (changed) { + mWorkSource.set(ws); + } + } + + if (changed && mHeld) { + try { + mService.updateWakeLockWorkSource(mToken, mWorkSource, mHistoryTag); + } catch (RemoteException e) { + } + } + } + } + + /** @hide */ + public void setTag(String tag) { + mTag = tag; + } + + /** @hide */ + public void setHistoryTag(String tag) { + mHistoryTag = tag; + } + + /** @hide */ + public void setUnimportantForLogging(boolean state) { + if (state) mFlags |= UNIMPORTANT_FOR_LOGGING; + else mFlags &= ~UNIMPORTANT_FOR_LOGGING; + } + + @Override + public String toString() { + synchronized (mToken) { + return "WakeLock{" + + Integer.toHexString(System.identityHashCode(this)) + + " held=" + mHeld + ", refCount=" + mCount + "}"; + } + } + } +} diff --git a/src/main/java/android/os/PowerManagerInternal.java b/src/main/java/android/os/PowerManagerInternal.java new file mode 100644 index 0000000..6f31768 --- /dev/null +++ b/src/main/java/android/os/PowerManagerInternal.java @@ -0,0 +1,135 @@ +/* + * 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 android.os; + +import android.view.Display; + +/** + * Power manager local system service interface. + * + * @hide Only for use within the system server. + */ +public abstract class PowerManagerInternal { + /** + * Wakefulness: The device is asleep. It can only be awoken by a call to wakeUp(). + * The screen should be off or in the process of being turned off by the display controller. + * The device typically passes through the dozing state first. + */ + public static final int WAKEFULNESS_ASLEEP = 0; + + /** + * Wakefulness: The device is fully awake. It can be put to sleep by a call to goToSleep(). + * When the user activity timeout expires, the device may start dreaming or go to sleep. + */ + public static final int WAKEFULNESS_AWAKE = 1; + + /** + * Wakefulness: The device is dreaming. It can be awoken by a call to wakeUp(), + * which ends the dream. The device goes to sleep when goToSleep() is called, when + * the dream ends or when unplugged. + * User activity may brighten the screen but does not end the dream. + */ + public static final int WAKEFULNESS_DREAMING = 2; + + /** + * Wakefulness: The device is dozing. It is almost asleep but is allowing a special + * low-power "doze" dream to run which keeps the display on but lets the application + * processor be suspended. It can be awoken by a call to wakeUp() which ends the dream. + * The device fully goes to sleep if the dream cannot be started or ends on its own. + */ + public static final int WAKEFULNESS_DOZING = 3; + + public static String wakefulnessToString(int wakefulness) { + switch (wakefulness) { + case WAKEFULNESS_ASLEEP: + return "Asleep"; + case WAKEFULNESS_AWAKE: + return "Awake"; + case WAKEFULNESS_DREAMING: + return "Dreaming"; + case WAKEFULNESS_DOZING: + return "Dozing"; + default: + return Integer.toString(wakefulness); + } + } + + /** + * Returns true if the wakefulness state represents an interactive state + * as defined by {@link android.os.PowerManager#isInteractive}. + */ + public static boolean isInteractive(int wakefulness) { + return wakefulness == WAKEFULNESS_AWAKE || wakefulness == WAKEFULNESS_DREAMING; + } + + /** + * Used by the window manager to override the screen brightness based on the + * current foreground activity. + * + * This method must only be called by the window manager. + * + * @param brightness The overridden brightness, or -1 to disable the override. + */ + public abstract void setScreenBrightnessOverrideFromWindowManager(int brightness); + + /** + * Used by the window manager to override the button brightness based on the + * current foreground activity. + * + * This method must only be called by the window manager. + * + * @param brightness The overridden brightness, or -1 to disable the override. + */ + public abstract void setButtonBrightnessOverrideFromWindowManager(int brightness); + + /** + * Used by the window manager to override the user activity timeout based on the + * current foreground activity. It can only be used to make the timeout shorter + * than usual, not longer. + * + * This method must only be called by the window manager. + * + * @param timeoutMillis The overridden timeout, or -1 to disable the override. + */ + public abstract void setUserActivityTimeoutOverrideFromWindowManager(long timeoutMillis); + + /** + * Used by device administration to set the maximum screen off timeout. + * + * This method must only be called by the device administration policy manager. + */ + public abstract void setMaximumScreenOffTimeoutFromDeviceAdmin(int timeMs); + + /** + * Used by the dream manager to override certain properties while dozing. + * + * @param screenState The overridden screen state, or {@link Display.STATE_UNKNOWN} + * to disable the override. + * @param screenBrightness The overridden screen brightness, or + * {@link PowerManager#BRIGHTNESS_DEFAULT} to disable the override. + */ + public abstract void setDozeOverrideFromDreamManager( + int screenState, int screenBrightness); + + public abstract boolean getLowPowerModeEnabled(); + + public abstract void registerLowPowerModeObserver(LowPowerModeListener listener); + + public interface LowPowerModeListener { + public void onLowPowerModeChanged(boolean enabled); + } +} diff --git a/src/main/java/android/os/PowerManagerTest.java b/src/main/java/android/os/PowerManagerTest.java new file mode 100644 index 0000000..9893c16 --- /dev/null +++ b/src/main/java/android/os/PowerManagerTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2008 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 android.os; + +import android.content.Context; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class PowerManagerTest extends AndroidTestCase { + + private PowerManager mPm; + + /** + * Setup any common data for the upcoming tests. + */ + @Override + public void setUp() throws Exception { + super.setUp(); + mPm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + } + + /** + * Confirm that the setup is good. + * + * @throws Exception + */ + @SmallTest + public void testPreconditions() throws Exception { + assertNotNull(mPm); + } + + /** + * Confirm that we can create functional wakelocks. + * + * @throws Exception + */ + @SmallTest + public void testNewWakeLock() throws Exception { + PowerManager.WakeLock wl = mPm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "FULL_WAKE_LOCK"); + doTestWakeLock(wl); + + wl = mPm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "SCREEN_BRIGHT_WAKE_LOCK"); + doTestWakeLock(wl); + + wl = mPm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "SCREEN_DIM_WAKE_LOCK"); + doTestWakeLock(wl); + + wl = mPm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PARTIAL_WAKE_LOCK"); + doTestWakeLock(wl); + + doTestSetBacklightBrightness(); + + // TODO: Some sort of functional test (maybe not in the unit test here?) + // that confirms that things are really happening e.g. screen power, keyboard power. +} + + /** + * Confirm that we can't create dysfunctional wakelocks. + * + * @throws Exception + */ + @SmallTest + public void testBadNewWakeLock() throws Exception { + + final int badFlags = PowerManager.SCREEN_BRIGHT_WAKE_LOCK + | PowerManager.SCREEN_DIM_WAKE_LOCK; + // wrap in try because we want the error here + try { + PowerManager.WakeLock wl = mPm.newWakeLock(badFlags, "foo"); + } catch (IllegalArgumentException e) { + return; + } + fail("Bad WakeLock flag was not caught."); + } + + /** + * Apply a few tests to a wakelock to make sure it's healthy. + * + * @param wl The wakelock to be tested. + */ + private void doTestWakeLock(PowerManager.WakeLock wl) { + // First try simple acquire/release + wl.acquire(); + assertTrue(wl.isHeld()); + wl.release(); + assertFalse(wl.isHeld()); + + // Try ref-counted acquire/release + wl.setReferenceCounted(true); + wl.acquire(); + assertTrue(wl.isHeld()); + wl.acquire(); + assertTrue(wl.isHeld()); + wl.release(); + assertTrue(wl.isHeld()); + wl.release(); + assertFalse(wl.isHeld()); + + // Try non-ref-counted + wl.setReferenceCounted(false); + wl.acquire(); + assertTrue(wl.isHeld()); + wl.acquire(); + assertTrue(wl.isHeld()); + wl.release(); + assertFalse(wl.isHeld()); + + // TODO: Threaded test (needs handler) to make sure timed wakelocks work too + } + + + /** + * Test that calling {@link android.os.IHardwareService#setBacklights(int)} requires + * permissions. + *

    Tests permission: + * {@link android.Manifest.permission#DEVICE_POWER} + */ + private void doTestSetBacklightBrightness() { + try { + mPm.setBacklightBrightness(0); + fail("setBacklights did not throw SecurityException as expected"); + } catch (SecurityException e) { + // expected + } + } + +} diff --git a/src/main/java/android/os/Process.java b/src/main/java/android/os/Process.java new file mode 100644 index 0000000..21a9904 --- /dev/null +++ b/src/main/java/android/os/Process.java @@ -0,0 +1,1144 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.system.Os; +import android.util.Log; +import com.android.internal.os.Zygote; +import java.io.BufferedWriter; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/*package*/ class ZygoteStartFailedEx extends Exception { + ZygoteStartFailedEx(String s) { + super(s); + } + + ZygoteStartFailedEx(Throwable cause) { + super(cause); + } + + ZygoteStartFailedEx(String s, Throwable cause) { + super(s, cause); + } +} + +/** + * Tools for managing OS processes. + */ +public class Process { + private static final String LOG_TAG = "Process"; + + /** + * @hide for internal use only. + */ + public static final String ZYGOTE_SOCKET = "zygote"; + + /** + * @hide for internal use only. + */ + public static final String SECONDARY_ZYGOTE_SOCKET = "zygote_secondary"; + + /** + * Defines the root UID. + * @hide + */ + public static final int ROOT_UID = 0; + + /** + * Defines the UID/GID under which system code runs. + */ + public static final int SYSTEM_UID = 1000; + + /** + * Defines the UID/GID under which the telephony code runs. + */ + public static final int PHONE_UID = 1001; + + /** + * Defines the UID/GID for the user shell. + * @hide + */ + public static final int SHELL_UID = 2000; + + /** + * Defines the UID/GID for the log group. + * @hide + */ + public static final int LOG_UID = 1007; + + /** + * Defines the UID/GID for the WIFI supplicant process. + * @hide + */ + public static final int WIFI_UID = 1010; + + /** + * Defines the UID/GID for the mediaserver process. + * @hide + */ + public static final int MEDIA_UID = 1013; + + /** + * Defines the UID/GID for the DRM process. + * @hide + */ + public static final int DRM_UID = 1019; + + /** + * Defines the UID/GID for the group that controls VPN services. + * @hide + */ + public static final int VPN_UID = 1016; + + /** + * Defines the UID/GID for the NFC service process. + * @hide + */ + public static final int NFC_UID = 1027; + + /** + * Defines the UID/GID for the Bluetooth service process. + * @hide + */ + public static final int BLUETOOTH_UID = 1002; + + /** + * Defines the GID for the group that allows write access to the internal media storage. + * @hide + */ + public static final int MEDIA_RW_GID = 1023; + + /** + * Access to installed package details + * @hide + */ + public static final int PACKAGE_INFO_GID = 1032; + + /** + * Defines the UID/GID for the shared RELRO file updater process. + * @hide + */ + public static final int SHARED_RELRO_UID = 1037; + + /** + * Defines the start of a range of UIDs (and GIDs), going from this + * number to {@link #LAST_APPLICATION_UID} that are reserved for assigning + * to applications. + */ + public static final int FIRST_APPLICATION_UID = 10000; + + /** + * Last of application-specific UIDs starting at + * {@link #FIRST_APPLICATION_UID}. + */ + public static final int LAST_APPLICATION_UID = 19999; + + /** + * First uid used for fully isolated sandboxed processes (with no permissions of their own) + * @hide + */ + public static final int FIRST_ISOLATED_UID = 99000; + + /** + * Last uid used for fully isolated sandboxed processes (with no permissions of their own) + * @hide + */ + public static final int LAST_ISOLATED_UID = 99999; + + /** + * Defines the gid shared by all applications running under the same profile. + * @hide + */ + public static final int SHARED_USER_GID = 9997; + + /** + * First gid for applications to share resources. Used when forward-locking + * is enabled but all UserHandles need to be able to read the resources. + * @hide + */ + public static final int FIRST_SHARED_APPLICATION_GID = 50000; + + /** + * Last gid for applications to share resources. Used when forward-locking + * is enabled but all UserHandles need to be able to read the resources. + * @hide + */ + public static final int LAST_SHARED_APPLICATION_GID = 59999; + + /** + * Standard priority of application threads. + * Use with {@link #setThreadPriority(int)} and + * {@link #setThreadPriority(int, int)}, not with the normal + * {@link java.lang.Thread} class. + */ + public static final int THREAD_PRIORITY_DEFAULT = 0; + + /* + * *************************************** + * ** Keep in sync with utils/threads.h ** + * *************************************** + */ + + /** + * Lowest available thread priority. Only for those who really, really + * don't want to run if anything else is happening. + * Use with {@link #setThreadPriority(int)} and + * {@link #setThreadPriority(int, int)}, not with the normal + * {@link java.lang.Thread} class. + */ + public static final int THREAD_PRIORITY_LOWEST = 19; + + /** + * Standard priority background threads. This gives your thread a slightly + * lower than normal priority, so that it will have less chance of impacting + * the responsiveness of the user interface. + * Use with {@link #setThreadPriority(int)} and + * {@link #setThreadPriority(int, int)}, not with the normal + * {@link java.lang.Thread} class. + */ + public static final int THREAD_PRIORITY_BACKGROUND = 10; + + /** + * Standard priority of threads that are currently running a user interface + * that the user is interacting with. Applications can not normally + * change to this priority; the system will automatically adjust your + * application threads as the user moves through the UI. + * Use with {@link #setThreadPriority(int)} and + * {@link #setThreadPriority(int, int)}, not with the normal + * {@link java.lang.Thread} class. + */ + public static final int THREAD_PRIORITY_FOREGROUND = -2; + + /** + * Standard priority of system display threads, involved in updating + * the user interface. Applications can not + * normally change to this priority. + * Use with {@link #setThreadPriority(int)} and + * {@link #setThreadPriority(int, int)}, not with the normal + * {@link java.lang.Thread} class. + */ + public static final int THREAD_PRIORITY_DISPLAY = -4; + + /** + * Standard priority of the most important display threads, for compositing + * the screen and retrieving input events. Applications can not normally + * change to this priority. + * Use with {@link #setThreadPriority(int)} and + * {@link #setThreadPriority(int, int)}, not with the normal + * {@link java.lang.Thread} class. + */ + public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8; + + /** + * Standard priority of audio threads. Applications can not normally + * change to this priority. + * Use with {@link #setThreadPriority(int)} and + * {@link #setThreadPriority(int, int)}, not with the normal + * {@link java.lang.Thread} class. + */ + public static final int THREAD_PRIORITY_AUDIO = -16; + + /** + * Standard priority of the most important audio threads. + * Applications can not normally change to this priority. + * Use with {@link #setThreadPriority(int)} and + * {@link #setThreadPriority(int, int)}, not with the normal + * {@link java.lang.Thread} class. + */ + public static final int THREAD_PRIORITY_URGENT_AUDIO = -19; + + /** + * Minimum increment to make a priority more favorable. + */ + public static final int THREAD_PRIORITY_MORE_FAVORABLE = -1; + + /** + * Minimum increment to make a priority less favorable. + */ + public static final int THREAD_PRIORITY_LESS_FAVORABLE = +1; + + /** + * Default scheduling policy + * @hide + */ + public static final int SCHED_OTHER = 0; + + /** + * First-In First-Out scheduling policy + * @hide + */ + public static final int SCHED_FIFO = 1; + + /** + * Round-Robin scheduling policy + * @hide + */ + public static final int SCHED_RR = 2; + + /** + * Batch scheduling policy + * @hide + */ + public static final int SCHED_BATCH = 3; + + /** + * Idle scheduling policy + * @hide + */ + public static final int SCHED_IDLE = 5; + + // Keep in sync with SP_* constants of enum type SchedPolicy + // declared in system/core/include/cutils/sched_policy.h, + // except THREAD_GROUP_DEFAULT does not correspond to any SP_* value. + + /** + * Default thread group - + * has meaning with setProcessGroup() only, cannot be used with setThreadGroup(). + * When used with setProcessGroup(), the group of each thread in the process + * is conditionally changed based on that thread's current priority, as follows: + * threads with priority numerically less than THREAD_PRIORITY_BACKGROUND + * are moved to foreground thread group. All other threads are left unchanged. + * @hide + */ + public static final int THREAD_GROUP_DEFAULT = -1; + + /** + * Background thread group - All threads in + * this group are scheduled with a reduced share of the CPU. + * Value is same as constant SP_BACKGROUND of enum SchedPolicy. + * FIXME rename to THREAD_GROUP_BACKGROUND. + * @hide + */ + public static final int THREAD_GROUP_BG_NONINTERACTIVE = 0; + + /** + * Foreground thread group - All threads in + * this group are scheduled with a normal share of the CPU. + * Value is same as constant SP_FOREGROUND of enum SchedPolicy. + * Not used at this level. + * @hide + **/ + private static final int THREAD_GROUP_FOREGROUND = 1; + + /** + * System thread group. + * @hide + **/ + public static final int THREAD_GROUP_SYSTEM = 2; + + /** + * Application audio thread group. + * @hide + **/ + public static final int THREAD_GROUP_AUDIO_APP = 3; + + /** + * System audio thread group. + * @hide + **/ + public static final int THREAD_GROUP_AUDIO_SYS = 4; + + public static final int SIGNAL_QUIT = 3; + public static final int SIGNAL_KILL = 9; + public static final int SIGNAL_USR1 = 10; + + /** + * State for communicating with the zygote process. + * + * @hide for internal use only. + */ + public static class ZygoteState { + final LocalSocket socket; + final DataInputStream inputStream; + final BufferedWriter writer; + final List abiList; + + boolean mClosed; + + private ZygoteState(LocalSocket socket, DataInputStream inputStream, + BufferedWriter writer, List abiList) { + this.socket = socket; + this.inputStream = inputStream; + this.writer = writer; + this.abiList = abiList; + } + + public static ZygoteState connect(String socketAddress) throws IOException { + DataInputStream zygoteInputStream = null; + BufferedWriter zygoteWriter = null; + final LocalSocket zygoteSocket = new LocalSocket(); + + try { + zygoteSocket.connect(new LocalSocketAddress(socketAddress, + LocalSocketAddress.Namespace.RESERVED)); + + zygoteInputStream = new DataInputStream(zygoteSocket.getInputStream()); + + zygoteWriter = new BufferedWriter(new OutputStreamWriter( + zygoteSocket.getOutputStream()), 256); + } catch (IOException ex) { + try { + zygoteSocket.close(); + } catch (IOException ignore) { + } + + throw ex; + } + + String abiListString = getAbiList(zygoteWriter, zygoteInputStream); + Log.i("Zygote", "Process: zygote socket opened, supported ABIS: " + abiListString); + + return new ZygoteState(zygoteSocket, zygoteInputStream, zygoteWriter, + Arrays.asList(abiListString.split(","))); + } + + boolean matches(String abi) { + return abiList.contains(abi); + } + + public void close() { + try { + socket.close(); + } catch (IOException ex) { + Log.e(LOG_TAG,"I/O exception on routine close", ex); + } + + mClosed = true; + } + + boolean isClosed() { + return mClosed; + } + } + + /** + * The state of the connection to the primary zygote. + */ + static ZygoteState primaryZygoteState; + + /** + * The state of the connection to the secondary zygote. + */ + static ZygoteState secondaryZygoteState; + + /** + * Start a new process. + * + *

    If processes are enabled, a new process is created and the + * static main() function of a processClass is executed there. + * The process will continue running after this function returns. + * + *

    If processes are not enabled, a new thread in the caller's + * process is created and main() of processClass called there. + * + *

    The niceName parameter, if not an empty string, is a custom name to + * give to the process instead of using processClass. This allows you to + * make easily identifyable processes even if you are using the same base + * processClass to start them. + * + * @param processClass The class to use as the process's main entry + * point. + * @param niceName A more readable name to use for the process. + * @param uid The user-id under which the process will run. + * @param gid The group-id under which the process will run. + * @param gids Additional group-ids associated with the process. + * @param debugFlags Additional flags. + * @param targetSdkVersion The target SDK version for the app. + * @param seInfo null-ok SELinux information for the new process. + * @param abi non-null the ABI this app should be started with. + * @param instructionSet null-ok the instruction set to use. + * @param appDataDir null-ok the data directory of the app. + * @param zygoteArgs Additional arguments to supply to the zygote process. + * + * @return An object that describes the result of the attempt to start the process. + * @throws RuntimeException on fatal start failure + * + * {@hide} + */ + public static final ProcessStartResult start(final String processClass, + final String niceName, + int uid, int gid, int[] gids, + int debugFlags, int mountExternal, + int targetSdkVersion, + String seInfo, + String abi, + String instructionSet, + String appDataDir, + String[] zygoteArgs) { + try { + return startViaZygote(processClass, niceName, uid, gid, gids, + debugFlags, mountExternal, targetSdkVersion, seInfo, + abi, instructionSet, appDataDir, zygoteArgs); + } catch (ZygoteStartFailedEx ex) { + Log.e(LOG_TAG, + "Starting VM process through Zygote failed"); + throw new RuntimeException( + "Starting VM process through Zygote failed", ex); + } + } + + /** retry interval for opening a zygote socket */ + static final int ZYGOTE_RETRY_MILLIS = 500; + + /** + * Queries the zygote for the list of ABIS it supports. + * + * @throws ZygoteStartFailedEx if the query failed. + */ + private static String getAbiList(BufferedWriter writer, DataInputStream inputStream) + throws IOException { + // Each query starts with the argument count (1 in this case) + writer.write("1"); + // ... followed by a new-line. + writer.newLine(); + // ... followed by our only argument. + writer.write("--query-abi-list"); + writer.newLine(); + writer.flush(); + + // The response is a length prefixed stream of ASCII bytes. + int numBytes = inputStream.readInt(); + byte[] bytes = new byte[numBytes]; + inputStream.readFully(bytes); + + return new String(bytes, StandardCharsets.US_ASCII); + } + + /** + * Sends an argument list to the zygote process, which starts a new child + * and returns the child's pid. Please note: the present implementation + * replaces newlines in the argument list with spaces. + * + * @throws ZygoteStartFailedEx if process start failed for any reason + */ + private static ProcessStartResult zygoteSendArgsAndGetResult( + ZygoteState zygoteState, ArrayList args) + throws ZygoteStartFailedEx { + try { + /** + * See com.android.internal.os.ZygoteInit.readArgumentList() + * Presently the wire format to the zygote process is: + * a) a count of arguments (argc, in essence) + * b) a number of newline-separated argument strings equal to count + * + * After the zygote process reads these it will write the pid of + * the child or -1 on failure, followed by boolean to + * indicate whether a wrapper process was used. + */ + final BufferedWriter writer = zygoteState.writer; + final DataInputStream inputStream = zygoteState.inputStream; + + writer.write(Integer.toString(args.size())); + writer.newLine(); + + int sz = args.size(); + for (int i = 0; i < sz; i++) { + String arg = args.get(i); + if (arg.indexOf('\n') >= 0) { + throw new ZygoteStartFailedEx( + "embedded newlines not allowed"); + } + writer.write(arg); + writer.newLine(); + } + + writer.flush(); + + // Should there be a timeout on this? + ProcessStartResult result = new ProcessStartResult(); + result.pid = inputStream.readInt(); + if (result.pid < 0) { + throw new ZygoteStartFailedEx("fork() failed"); + } + result.usingWrapper = inputStream.readBoolean(); + return result; + } catch (IOException ex) { + zygoteState.close(); + throw new ZygoteStartFailedEx(ex); + } + } + + /** + * Starts a new process via the zygote mechanism. + * + * @param processClass Class name whose static main() to run + * @param niceName 'nice' process name to appear in ps + * @param uid a POSIX uid that the new process should setuid() to + * @param gid a POSIX gid that the new process shuold setgid() to + * @param gids null-ok; a list of supplementary group IDs that the + * new process should setgroup() to. + * @param debugFlags Additional flags. + * @param targetSdkVersion The target SDK version for the app. + * @param seInfo null-ok SELinux information for the new process. + * @param abi the ABI the process should use. + * @param instructionSet null-ok the instruction set to use. + * @param appDataDir null-ok the data directory of the app. + * @param extraArgs Additional arguments to supply to the zygote process. + * @return An object that describes the result of the attempt to start the process. + * @throws ZygoteStartFailedEx if process start failed for any reason + */ + private static ProcessStartResult startViaZygote(final String processClass, + final String niceName, + final int uid, final int gid, + final int[] gids, + int debugFlags, int mountExternal, + int targetSdkVersion, + String seInfo, + String abi, + String instructionSet, + String appDataDir, + String[] extraArgs) + throws ZygoteStartFailedEx { + synchronized(Process.class) { + ArrayList argsForZygote = new ArrayList(); + + // --runtime-init, --setuid=, --setgid=, + // and --setgroups= must go first + argsForZygote.add("--runtime-init"); + argsForZygote.add("--setuid=" + uid); + argsForZygote.add("--setgid=" + gid); + if ((debugFlags & Zygote.DEBUG_ENABLE_JNI_LOGGING) != 0) { + argsForZygote.add("--enable-jni-logging"); + } + if ((debugFlags & Zygote.DEBUG_ENABLE_SAFEMODE) != 0) { + argsForZygote.add("--enable-safemode"); + } + if ((debugFlags & Zygote.DEBUG_ENABLE_DEBUGGER) != 0) { + argsForZygote.add("--enable-debugger"); + } + if ((debugFlags & Zygote.DEBUG_ENABLE_CHECKJNI) != 0) { + argsForZygote.add("--enable-checkjni"); + } + if ((debugFlags & Zygote.DEBUG_ENABLE_ASSERT) != 0) { + argsForZygote.add("--enable-assert"); + } + if (mountExternal == Zygote.MOUNT_EXTERNAL_MULTIUSER) { + argsForZygote.add("--mount-external-multiuser"); + } else if (mountExternal == Zygote.MOUNT_EXTERNAL_MULTIUSER_ALL) { + argsForZygote.add("--mount-external-multiuser-all"); + } + argsForZygote.add("--target-sdk-version=" + targetSdkVersion); + + //TODO optionally enable debuger + //argsForZygote.add("--enable-debugger"); + + // --setgroups is a comma-separated list + if (gids != null && gids.length > 0) { + StringBuilder sb = new StringBuilder(); + sb.append("--setgroups="); + + int sz = gids.length; + for (int i = 0; i < sz; i++) { + if (i != 0) { + sb.append(','); + } + sb.append(gids[i]); + } + + argsForZygote.add(sb.toString()); + } + + if (niceName != null) { + argsForZygote.add("--nice-name=" + niceName); + } + + if (seInfo != null) { + argsForZygote.add("--seinfo=" + seInfo); + } + + if (instructionSet != null) { + argsForZygote.add("--instruction-set=" + instructionSet); + } + + if (appDataDir != null) { + argsForZygote.add("--app-data-dir=" + appDataDir); + } + + argsForZygote.add(processClass); + + if (extraArgs != null) { + for (String arg : extraArgs) { + argsForZygote.add(arg); + } + } + + return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote); + } + } + + /** + * Tries to establish a connection to the zygote that handles a given {@code abi}. Might block and retry if the + * zygote is unresponsive. This method is a no-op if a connection is already open. + * + * @hide + */ + public static void establishZygoteConnectionForAbi(String abi) { + try { + openZygoteSocketIfNeeded(abi); + } catch (ZygoteStartFailedEx ex) { + throw new RuntimeException("Unable to connect to zygote for abi: " + abi, ex); + } + } + + /** + * Tries to open socket to Zygote process if not already open. If + * already open, does nothing. May block and retry. + */ + private static ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx { + if (primaryZygoteState == null || primaryZygoteState.isClosed()) { + try { + primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET); + } catch (IOException ioe) { + throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe); + } + } + + if (primaryZygoteState.matches(abi)) { + return primaryZygoteState; + } + + // The primary zygote didn't match. Try the secondary. + if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) { + try { + secondaryZygoteState = ZygoteState.connect(SECONDARY_ZYGOTE_SOCKET); + } catch (IOException ioe) { + throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe); + } + } + + if (secondaryZygoteState.matches(abi)) { + return secondaryZygoteState; + } + + throw new ZygoteStartFailedEx("Unsupported zygote ABI: " + abi); + } + + /** + * Returns elapsed milliseconds of the time this process has run. + * @return Returns the number of milliseconds this process has return. + */ + public static final native long getElapsedCpuTime(); + + /** + * Returns the identifier of this process, which can be used with + * {@link #killProcess} and {@link #sendSignal}. + */ + public static final int myPid() { + return Os.getpid(); + } + + /** + * Returns the identifier of this process' parent. + * @hide + */ + public static final int myPpid() { + return Os.getppid(); + } + + /** + * Returns the identifier of the calling thread, which be used with + * {@link #setThreadPriority(int, int)}. + */ + public static final int myTid() { + return Os.gettid(); + } + + /** + * Returns the identifier of this process's uid. This is the kernel uid + * that the process is running under, which is the identity of its + * app-specific sandbox. It is different from {@link #myUserHandle} in that + * a uid identifies a specific app sandbox in a specific user. + */ + public static final int myUid() { + return Os.getuid(); + } + + /** + * Returns this process's user handle. This is the + * user the process is running under. It is distinct from + * {@link #myUid()} in that a particular user will have multiple + * distinct apps running under it each with their own uid. + */ + public static final UserHandle myUserHandle() { + return new UserHandle(UserHandle.getUserId(myUid())); + } + + /** + * Returns whether the current process is in an isolated sandbox. + * @hide + */ + public static final boolean isIsolated() { + int uid = UserHandle.getAppId(myUid()); + return uid >= FIRST_ISOLATED_UID && uid <= LAST_ISOLATED_UID; + } + + /** + * Returns the UID assigned to a particular user name, or -1 if there is + * none. If the given string consists of only numbers, it is converted + * directly to a uid. + */ + public static final native int getUidForName(String name); + + /** + * Returns the GID assigned to a particular user name, or -1 if there is + * none. If the given string consists of only numbers, it is converted + * directly to a gid. + */ + public static final native int getGidForName(String name); + + /** + * Returns a uid for a currently running process. + * @param pid the process id + * @return the uid of the process, or -1 if the process is not running. + * @hide pending API council review + */ + public static final int getUidForPid(int pid) { + String[] procStatusLabels = { "Uid:" }; + long[] procStatusValues = new long[1]; + procStatusValues[0] = -1; + Process.readProcLines("/proc/" + pid + "/status", procStatusLabels, procStatusValues); + return (int) procStatusValues[0]; + } + + /** + * Returns the parent process id for a currently running process. + * @param pid the process id + * @return the parent process id of the process, or -1 if the process is not running. + * @hide + */ + public static final int getParentPid(int pid) { + String[] procStatusLabels = { "PPid:" }; + long[] procStatusValues = new long[1]; + procStatusValues[0] = -1; + Process.readProcLines("/proc/" + pid + "/status", procStatusLabels, procStatusValues); + return (int) procStatusValues[0]; + } + + /** + * Returns the thread group leader id for a currently running thread. + * @param tid the thread id + * @return the thread group leader id of the thread, or -1 if the thread is not running. + * This is same as what getpid(2) would return if called by tid. + * @hide + */ + public static final int getThreadGroupLeader(int tid) { + String[] procStatusLabels = { "Tgid:" }; + long[] procStatusValues = new long[1]; + procStatusValues[0] = -1; + Process.readProcLines("/proc/" + tid + "/status", procStatusLabels, procStatusValues); + return (int) procStatusValues[0]; + } + + /** + * Set the priority of a thread, based on Linux priorities. + * + * @param tid The identifier of the thread/process to change. + * @param priority A Linux priority level, from -20 for highest scheduling + * priority to 19 for lowest scheduling priority. + * + * @throws IllegalArgumentException Throws IllegalArgumentException if + * tid does not exist. + * @throws SecurityException Throws SecurityException if your process does + * not have permission to modify the given thread, or to use the given + * priority. + */ + public static final native void setThreadPriority(int tid, int priority) + throws IllegalArgumentException, SecurityException; + + /** + * Call with 'false' to cause future calls to {@link #setThreadPriority(int)} to + * throw an exception if passed a background-level thread priority. This is only + * effective if the JNI layer is built with GUARD_THREAD_PRIORITY defined to 1. + * + * @hide + */ + public static final native void setCanSelfBackground(boolean backgroundOk); + + /** + * Sets the scheduling group for a thread. + * @hide + * @param tid The identifier of the thread to change. + * @param group The target group for this thread from THREAD_GROUP_*. + * + * @throws IllegalArgumentException Throws IllegalArgumentException if + * tid does not exist. + * @throws SecurityException Throws SecurityException if your process does + * not have permission to modify the given thread, or to use the given + * priority. + * If the thread is a thread group leader, that is it's gettid() == getpid(), + * then the other threads in the same thread group are _not_ affected. + */ + public static final native void setThreadGroup(int tid, int group) + throws IllegalArgumentException, SecurityException; + + /** + * Sets the scheduling group for a process and all child threads + * @hide + * @param pid The identifier of the process to change. + * @param group The target group for this process from THREAD_GROUP_*. + * + * @throws IllegalArgumentException Throws IllegalArgumentException if + * tid does not exist. + * @throws SecurityException Throws SecurityException if your process does + * not have permission to modify the given thread, or to use the given + * priority. + * + * group == THREAD_GROUP_DEFAULT means to move all non-background priority + * threads to the foreground scheduling group, but to leave background + * priority threads alone. group == THREAD_GROUP_BG_NONINTERACTIVE moves all + * threads, regardless of priority, to the background scheduling group. + * group == THREAD_GROUP_FOREGROUND is not allowed. + */ + public static final native void setProcessGroup(int pid, int group) + throws IllegalArgumentException, SecurityException; + + /** + * Return the scheduling group of requested process. + * + * @hide + */ + public static final native int getProcessGroup(int pid) + throws IllegalArgumentException, SecurityException; + + /** + * Set the priority of the calling thread, based on Linux priorities. See + * {@link #setThreadPriority(int, int)} for more information. + * + * @param priority A Linux priority level, from -20 for highest scheduling + * priority to 19 for lowest scheduling priority. + * + * @throws IllegalArgumentException Throws IllegalArgumentException if + * tid does not exist. + * @throws SecurityException Throws SecurityException if your process does + * not have permission to modify the given thread, or to use the given + * priority. + * + * @see #setThreadPriority(int, int) + */ + public static final native void setThreadPriority(int priority) + throws IllegalArgumentException, SecurityException; + + /** + * Return the current priority of a thread, based on Linux priorities. + * + * @param tid The identifier of the thread/process to change. + * + * @return Returns the current priority, as a Linux priority level, + * from -20 for highest scheduling priority to 19 for lowest scheduling + * priority. + * + * @throws IllegalArgumentException Throws IllegalArgumentException if + * tid does not exist. + */ + public static final native int getThreadPriority(int tid) + throws IllegalArgumentException; + + /** + * Set the scheduling policy and priority of a thread, based on Linux. + * + * @param tid The identifier of the thread/process to change. + * @param policy A Linux scheduling policy such as SCHED_OTHER etc. + * @param priority A Linux priority level in a range appropriate for the given policy. + * + * @throws IllegalArgumentException Throws IllegalArgumentException if + * tid does not exist, or if priority is out of range for the policy. + * @throws SecurityException Throws SecurityException if your process does + * not have permission to modify the given thread, or to use the given + * scheduling policy or priority. + * + * {@hide} + */ + public static final native void setThreadScheduler(int tid, int policy, int priority) + throws IllegalArgumentException; + + /** + * Determine whether the current environment supports multiple processes. + * + * @return Returns true if the system can run in multiple processes, else + * false if everything is running in a single process. + * + * @deprecated This method always returns true. Do not use. + */ + @Deprecated + public static final boolean supportsProcesses() { + return true; + } + + /** + * Adjust the swappiness level for a process. + * + * @param pid The process identifier to set. + * @param is_increased Whether swappiness should be increased or default. + * + * @return Returns true if the underlying system supports this + * feature, else false. + * + * {@hide} + */ + public static final native boolean setSwappiness(int pid, boolean is_increased); + + /** + * Change this process's argv[0] parameter. This can be useful to show + * more descriptive information in things like the 'ps' command. + * + * @param text The new name of this process. + * + * {@hide} + */ + public static final native void setArgV0(String text); + + /** + * Kill the process with the given PID. + * Note that, though this API allows us to request to + * kill any process based on its PID, the kernel will + * still impose standard restrictions on which PIDs you + * are actually able to kill. Typically this means only + * the process running the caller's packages/application + * and any additional processes created by that app; packages + * sharing a common UID will also be able to kill each + * other's processes. + */ + public static final void killProcess(int pid) { + sendSignal(pid, SIGNAL_KILL); + } + + /** @hide */ + public static final native int setUid(int uid); + + /** @hide */ + public static final native int setGid(int uid); + + /** + * Send a signal to the given process. + * + * @param pid The pid of the target process. + * @param signal The signal to send. + */ + public static final native void sendSignal(int pid, int signal); + + /** + * @hide + * Private impl for avoiding a log message... DO NOT USE without doing + * your own log, or the Android Illuminati will find you some night and + * beat you up. + */ + public static final void killProcessQuiet(int pid) { + sendSignalQuiet(pid, SIGNAL_KILL); + } + + /** + * @hide + * Private impl for avoiding a log message... DO NOT USE without doing + * your own log, or the Android Illuminati will find you some night and + * beat you up. + */ + public static final native void sendSignalQuiet(int pid, int signal); + + /** @hide */ + public static final native long getFreeMemory(); + + /** @hide */ + public static final native long getTotalMemory(); + + /** @hide */ + public static final native void readProcLines(String path, + String[] reqFields, long[] outSizes); + + /** @hide */ + public static final native int[] getPids(String path, int[] lastArray); + + /** @hide */ + public static final int PROC_TERM_MASK = 0xff; + /** @hide */ + public static final int PROC_ZERO_TERM = 0; + /** @hide */ + public static final int PROC_SPACE_TERM = (int)' '; + /** @hide */ + public static final int PROC_TAB_TERM = (int)'\t'; + /** @hide */ + public static final int PROC_COMBINE = 0x100; + /** @hide */ + public static final int PROC_PARENS = 0x200; + /** @hide */ + public static final int PROC_QUOTES = 0x400; + /** @hide */ + public static final int PROC_OUT_STRING = 0x1000; + /** @hide */ + public static final int PROC_OUT_LONG = 0x2000; + /** @hide */ + public static final int PROC_OUT_FLOAT = 0x4000; + + /** @hide */ + public static final native boolean readProcFile(String file, int[] format, + String[] outStrings, long[] outLongs, float[] outFloats); + + /** @hide */ + public static final native boolean parseProcLine(byte[] buffer, int startIndex, + int endIndex, int[] format, String[] outStrings, long[] outLongs, float[] outFloats); + + /** @hide */ + public static final native int[] getPidsForCommands(String[] cmds); + + /** + * Gets the total Pss value for a given process, in bytes. + * + * @param pid the process to the Pss for + * @return the total Pss value for the given process in bytes, + * or -1 if the value cannot be determined + * @hide + */ + public static final native long getPss(int pid); + + /** + * Specifies the outcome of having started a process. + * @hide + */ + public static final class ProcessStartResult { + /** + * The PID of the newly started process. + * Always >= 0. (If the start failed, an exception will have been thrown instead.) + */ + public int pid; + + /** + * True if the process was started with a wrapper attached. + */ + public boolean usingWrapper; + } + + /** + * Kill all processes in a process group started for the given + * pid. + * @hide + */ + public static final native int killProcessGroup(int uid, int pid); + + /** + * Remove all process groups. Expected to be called when ActivityManager + * is restarted. + * @hide + */ + public static final native void removeAllProcessGroups(); +} diff --git a/src/main/java/android/os/ProcessTest.java b/src/main/java/android/os/ProcessTest.java new file mode 100644 index 0000000..1f5b7c8 --- /dev/null +++ b/src/main/java/android/os/ProcessTest.java @@ -0,0 +1,54 @@ +/* + * 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 android.os; + +import android.os.Process; +import android.os.UserHandle; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; + +import junit.framework.TestCase; + + +public class ProcessTest extends TestCase { + + @MediumTest + public void testProcessGetUidFromName() throws Exception { + assertEquals(android.os.Process.SYSTEM_UID, Process.getUidForName("system")); + assertEquals(Process.BLUETOOTH_UID, Process.getUidForName("bluetooth")); + assertEquals(Process.FIRST_APPLICATION_UID, Process.getUidForName("u0_a0")); + assertEquals(UserHandle.getUid(1, Process.SYSTEM_UID), Process.getUidForName("u1_system")); + assertEquals(UserHandle.getUid(2, Process.FIRST_ISOLATED_UID), + Process.getUidForName("u2_i0")); + assertEquals(UserHandle.getUid(3, Process.FIRST_APPLICATION_UID + 100), + Process.getUidForName("u3_a100")); + } + + @MediumTest + public void testProcessGetUidFromNameFailure() throws Exception { + // Failure cases + assertEquals(-1, Process.getUidForName("u2a_foo")); + assertEquals(-1, Process.getUidForName("u1_abcdef")); + assertEquals(-1, Process.getUidForName("u23")); + assertEquals(-1, Process.getUidForName("u2_i34a")); + assertEquals(-1, Process.getUidForName("akjhwiuefhiuhsf")); + assertEquals(-1, Process.getUidForName("u5_radio5")); + assertEquals(-1, Process.getUidForName("u2jhsajhfkjhsafkhskafhkashfkjashfkjhaskjfdhakj3")); + } + +} diff --git a/src/main/java/android/os/RecoverySystem.java b/src/main/java/android/os/RecoverySystem.java new file mode 100644 index 0000000..b879c83 --- /dev/null +++ b/src/main/java/android/os/RecoverySystem.java @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2010 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 android.os; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.apache.harmony.security.asn1.BerInputStream; +import org.apache.harmony.security.pkcs7.ContentInfo; +import org.apache.harmony.security.pkcs7.SignedData; +import org.apache.harmony.security.pkcs7.SignerInfo; +import org.apache.harmony.security.x509.Certificate; + +/** + * RecoverySystem contains methods for interacting with the Android + * recovery system (the separate partition that can be used to install + * system updates, wipe user data, etc.) + */ +public class RecoverySystem { + private static final String TAG = "RecoverySystem"; + + /** + * Default location of zip file containing public keys (X509 + * certs) authorized to sign OTA updates. + */ + private static final File DEFAULT_KEYSTORE = + new File("/system/etc/security/otacerts.zip"); + + /** Send progress to listeners no more often than this (in ms). */ + private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; + + /** Used to communicate with recovery. See bootable/recovery/recovery.c. */ + private static File RECOVERY_DIR = new File("/cache/recovery"); + private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); + private static File LOG_FILE = new File(RECOVERY_DIR, "log"); + private static String LAST_PREFIX = "last_"; + + // Length limits for reading files. + private static int LOG_FILE_MAX_LENGTH = 64 * 1024; + + /** + * Interface definition for a callback to be invoked regularly as + * verification proceeds. + */ + public interface ProgressListener { + /** + * Called periodically as the verification progresses. + * + * @param progress the approximate percentage of the + * verification that has been completed, ranging from 0 + * to 100 (inclusive). + */ + public void onProgress(int progress); + } + + /** @return the set of certs that can be used to sign an OTA package. */ + private static HashSet getTrustedCerts(File keystore) + throws IOException, GeneralSecurityException { + HashSet trusted = new HashSet(); + if (keystore == null) { + keystore = DEFAULT_KEYSTORE; + } + ZipFile zip = new ZipFile(keystore); + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + InputStream is = zip.getInputStream(entry); + try { + trusted.add((X509Certificate) cf.generateCertificate(is)); + } finally { + is.close(); + } + } + } finally { + zip.close(); + } + return trusted; + } + + /** + * Verify the cryptographic signature of a system update package + * before installing it. Note that the package is also verified + * separately by the installer once the device is rebooted into + * the recovery system. This function will return only if the + * package was successfully verified; otherwise it will throw an + * exception. + * + * Verification of a package can take significant time, so this + * function should not be called from a UI thread. Interrupting + * the thread while this function is in progress will result in a + * SecurityException being thrown (and the thread's interrupt flag + * will be cleared). + * + * @param packageFile the package to be verified + * @param listener an object to receive periodic progress + * updates as verification proceeds. May be null. + * @param deviceCertsZipFile the zip file of certificates whose + * public keys we will accept. Verification succeeds if the + * package is signed by the private key corresponding to any + * public key in this file. May be null to use the system default + * file (currently "/system/etc/security/otacerts.zip"). + * + * @throws IOException if there were any errors reading the + * package or certs files. + * @throws GeneralSecurityException if verification failed + */ + public static void verifyPackage(File packageFile, + ProgressListener listener, + File deviceCertsZipFile) + throws IOException, GeneralSecurityException { + long fileLen = packageFile.length(); + + RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); + try { + int lastPercent = 0; + long lastPublishTime = System.currentTimeMillis(); + if (listener != null) { + listener.onProgress(lastPercent); + } + + raf.seek(fileLen - 6); + byte[] footer = new byte[6]; + raf.readFully(footer); + + if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) { + throw new SignatureException("no signature in file (no footer)"); + } + + int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); + int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); + + byte[] eocd = new byte[commentSize + 22]; + raf.seek(fileLen - (commentSize + 22)); + raf.readFully(eocd); + + // Check that we have found the start of the + // end-of-central-directory record. + if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b || + eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) { + throw new SignatureException("no signature in file (bad footer)"); + } + + for (int i = 4; i < eocd.length-3; ++i) { + if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b && + eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) { + throw new SignatureException("EOCD marker found after start of EOCD"); + } + } + + // The following code is largely copied from + // JarUtils.verifySignature(). We could just *call* that + // method here if that function didn't read the entire + // input (ie, the whole OTA package) into memory just to + // compute its message digest. + + BerInputStream bis = new BerInputStream( + new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); + ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis); + SignedData signedData = info.getSignedData(); + if (signedData == null) { + throw new IOException("signedData is null"); + } + List encCerts = signedData.getCertificates(); + if (encCerts.isEmpty()) { + throw new IOException("encCerts is empty"); + } + // Take the first certificate from the signature (packages + // should contain only one). + Iterator it = encCerts.iterator(); + X509Certificate cert = null; + if (it.hasNext()) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + InputStream is = new ByteArrayInputStream(it.next().getEncoded()); + cert = (X509Certificate) cf.generateCertificate(is); + } else { + throw new SignatureException("signature contains no certificates"); + } + + List sigInfos = signedData.getSignerInfos(); + SignerInfo sigInfo; + if (!sigInfos.isEmpty()) { + sigInfo = (SignerInfo)sigInfos.get(0); + } else { + throw new IOException("no signer infos!"); + } + + // Check that the public key of the certificate contained + // in the package equals one of our trusted public keys. + + HashSet trusted = getTrustedCerts( + deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); + + PublicKey signatureKey = cert.getPublicKey(); + boolean verified = false; + for (X509Certificate c : trusted) { + if (c.getPublicKey().equals(signatureKey)) { + verified = true; + break; + } + } + if (!verified) { + throw new SignatureException("signature doesn't match any trusted key"); + } + + // The signature cert matches a trusted key. Now verify that + // the digest in the cert matches the actual file data. + + // The verifier in recovery only handles SHA1withRSA and + // SHA256withRSA signatures. SignApk chooses which to use + // based on the signature algorithm of the cert: + // + // "SHA256withRSA" cert -> "SHA256withRSA" signature + // "SHA1withRSA" cert -> "SHA1withRSA" signature + // "MD5withRSA" cert -> "SHA1withRSA" signature (for backwards compatibility) + // any other cert -> SignApk fails + // + // Here we ignore whatever the cert says, and instead use + // whatever algorithm is used by the signature. + + String da = sigInfo.getDigestAlgorithm(); + String dea = sigInfo.getDigestEncryptionAlgorithm(); + String alg = null; + if (da == null || dea == null) { + // fall back to the cert algorithm if the sig one + // doesn't look right. + alg = cert.getSigAlgName(); + } else { + alg = da + "with" + dea; + } + Signature sig = Signature.getInstance(alg); + sig.initVerify(cert); + + // The signature covers all of the OTA package except the + // archive comment and its 2-byte length. + long toRead = fileLen - commentSize - 2; + long soFar = 0; + raf.seek(0); + byte[] buffer = new byte[4096]; + boolean interrupted = false; + while (soFar < toRead) { + interrupted = Thread.interrupted(); + if (interrupted) break; + int size = buffer.length; + if (soFar + size > toRead) { + size = (int)(toRead - soFar); + } + int read = raf.read(buffer, 0, size); + sig.update(buffer, 0, read); + soFar += read; + + if (listener != null) { + long now = System.currentTimeMillis(); + int p = (int)(soFar * 100 / toRead); + if (p > lastPercent && + now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { + lastPercent = p; + lastPublishTime = now; + listener.onProgress(lastPercent); + } + } + } + if (listener != null) { + listener.onProgress(100); + } + + if (interrupted) { + throw new SignatureException("verification was interrupted"); + } + + if (!sig.verify(sigInfo.getEncryptedDigest())) { + throw new SignatureException("signature digest verification failed"); + } + } finally { + raf.close(); + } + } + + /** + * Reboots the device in order to install the given update + * package. + * Requires the {@link android.Manifest.permission#REBOOT} permission. + * + * @param context the Context to use + * @param packageFile the update package to install. Must be on + * a partition mountable by recovery. (The set of partitions + * known to recovery may vary from device to device. Generally, + * /cache and /data are safe.) + * + * @throws IOException if writing the recovery command file + * fails, or if the reboot itself fails. + */ + public static void installPackage(Context context, File packageFile) + throws IOException { + String filename = packageFile.getCanonicalPath(); + Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); + + final String filenameArg = "--update_package=" + filename; + final String localeArg = "--locale=" + Locale.getDefault().toString(); + bootCommand(context, filenameArg, localeArg); + } + + /** + * Reboots the device and wipes the user data and cache + * partitions. This is sometimes called a "factory reset", which + * is something of a misnomer because the system partition is not + * restored to its factory state. Requires the + * {@link android.Manifest.permission#REBOOT} permission. + * + * @param context the Context to use + * + * @throws IOException if writing the recovery command file + * fails, or if the reboot itself fails. + * @throws SecurityException if the current user is not allowed to wipe data. + */ + public static void rebootWipeUserData(Context context) throws IOException { + rebootWipeUserData(context, false, context.getPackageName()); + } + + /** {@hide} */ + public static void rebootWipeUserData(Context context, String reason) throws IOException { + rebootWipeUserData(context, false, reason); + } + + /** {@hide} */ + public static void rebootWipeUserData(Context context, boolean shutdown) + throws IOException { + rebootWipeUserData(context, shutdown, context.getPackageName()); + } + + /** + * Reboots the device and wipes the user data and cache + * partitions. This is sometimes called a "factory reset", which + * is something of a misnomer because the system partition is not + * restored to its factory state. Requires the + * {@link android.Manifest.permission#REBOOT} permission. + * + * @param context the Context to use + * @param shutdown if true, the device will be powered down after + * the wipe completes, rather than being rebooted + * back to the regular system. + * + * @throws IOException if writing the recovery command file + * fails, or if the reboot itself fails. + * @throws SecurityException if the current user is not allowed to wipe data. + * + * @hide + */ + public static void rebootWipeUserData(Context context, boolean shutdown, String reason) + throws IOException { + UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); + if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) { + throw new SecurityException("Wiping data is not allowed for this user."); + } + final ConditionVariable condition = new ConditionVariable(); + + Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION"); + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER, + android.Manifest.permission.MASTER_CLEAR, + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + condition.open(); + } + }, null, 0, null, null); + + // Block until the ordered broadcast has completed. + condition.block(); + + String shutdownArg = null; + if (shutdown) { + shutdownArg = "--shutdown_after"; + } + + String reasonArg = null; + if (!TextUtils.isEmpty(reason)) { + reasonArg = "--reason=" + sanitizeArg(reason); + } + + final String localeArg = "--locale=" + Locale.getDefault().toString(); + bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg); + } + + /** + * Reboot into the recovery system to wipe the /cache partition. + * @throws IOException if something goes wrong. + */ + public static void rebootWipeCache(Context context) throws IOException { + rebootWipeCache(context, context.getPackageName()); + } + + /** {@hide} */ + public static void rebootWipeCache(Context context, String reason) throws IOException { + String reasonArg = null; + if (!TextUtils.isEmpty(reason)) { + reasonArg = "--reason=" + sanitizeArg(reason); + } + + final String localeArg = "--locale=" + Locale.getDefault().toString(); + bootCommand(context, "--wipe_cache", reasonArg, localeArg); + } + + /** + * Reboot into the recovery system with the supplied argument. + * @param args to pass to the recovery utility. + * @throws IOException if something goes wrong. + */ + private static void bootCommand(Context context, String... args) throws IOException { + RECOVERY_DIR.mkdirs(); // In case we need it + COMMAND_FILE.delete(); // In case it's not writable + LOG_FILE.delete(); + + FileWriter command = new FileWriter(COMMAND_FILE); + try { + for (String arg : args) { + if (!TextUtils.isEmpty(arg)) { + command.write(arg); + command.write("\n"); + } + } + } finally { + command.close(); + } + + // Having written the command file, go ahead and reboot + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + pm.reboot(PowerManager.REBOOT_RECOVERY); + + throw new IOException("Reboot failed (no permissions?)"); + } + + /** + * Called after booting to process and remove recovery-related files. + * @return the log file from recovery, or null if none was found. + * + * @hide + */ + public static String handleAftermath() { + // Record the tail of the LOG_FILE + String log = null; + try { + log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n"); + } catch (FileNotFoundException e) { + Log.i(TAG, "No recovery log file"); + } catch (IOException e) { + Log.e(TAG, "Error reading recovery log", e); + } + + // Delete everything in RECOVERY_DIR except those beginning + // with LAST_PREFIX + String[] names = RECOVERY_DIR.list(); + for (int i = 0; names != null && i < names.length; i++) { + if (names[i].startsWith(LAST_PREFIX)) continue; + File f = new File(RECOVERY_DIR, names[i]); + if (!f.delete()) { + Log.e(TAG, "Can't delete: " + f); + } else { + Log.i(TAG, "Deleted: " + f); + } + } + + return log; + } + + /** + * Internally, recovery treats each line of the command file as a separate + * argv, so we only need to protect against newlines and nulls. + */ + private static String sanitizeArg(String arg) { + arg = arg.replace('\0', '?'); + arg = arg.replace('\n', '?'); + return arg; + } + + private void RecoverySystem() { } // Do not instantiate +} diff --git a/src/main/java/android/os/Registrant.java b/src/main/java/android/os/Registrant.java new file mode 100644 index 0000000..705cc5d --- /dev/null +++ b/src/main/java/android/os/Registrant.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.os.Handler; +import android.os.Message; + +import java.lang.ref.WeakReference; + +/** @hide */ +public class Registrant +{ + public + Registrant(Handler h, int what, Object obj) + { + refH = new WeakReference(h); + this.what = what; + userObj = obj; + } + + public void + clear() + { + refH = null; + userObj = null; + } + + public void + notifyRegistrant() + { + internalNotifyRegistrant (null, null); + } + + public void + notifyResult(Object result) + { + internalNotifyRegistrant (result, null); + } + + public void + notifyException(Throwable exception) + { + internalNotifyRegistrant (null, exception); + } + + /** + * This makes a copy of @param ar + */ + public void + notifyRegistrant(AsyncResult ar) + { + internalNotifyRegistrant (ar.result, ar.exception); + } + + /*package*/ void + internalNotifyRegistrant (Object result, Throwable exception) + { + Handler h = getHandler(); + + if (h == null) { + clear(); + } else { + Message msg = Message.obtain(); + + msg.what = what; + + msg.obj = new AsyncResult(userObj, result, exception); + + h.sendMessage(msg); + } + } + + /** + * NOTE: May return null if weak reference has been collected + */ + + public Message + messageForRegistrant() + { + Handler h = getHandler(); + + if (h == null) { + clear(); + + return null; + } else { + Message msg = h.obtainMessage(); + + msg.what = what; + msg.obj = userObj; + + return msg; + } + } + + public Handler + getHandler() + { + if (refH == null) + return null; + + return (Handler) refH.get(); + } + + WeakReference refH; + int what; + Object userObj; +} + diff --git a/src/main/java/android/os/RegistrantList.java b/src/main/java/android/os/RegistrantList.java new file mode 100644 index 0000000..9ab61f5 --- /dev/null +++ b/src/main/java/android/os/RegistrantList.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.os.Handler; + +import java.util.ArrayList; + +/** @hide */ +public class RegistrantList +{ + ArrayList registrants = new ArrayList(); // of Registrant + + public synchronized void + add(Handler h, int what, Object obj) + { + add(new Registrant(h, what, obj)); + } + + public synchronized void + addUnique(Handler h, int what, Object obj) + { + // if the handler is already in the registrant list, remove it + remove(h); + add(new Registrant(h, what, obj)); + } + + public synchronized void + add(Registrant r) + { + removeCleared(); + registrants.add(r); + } + + public synchronized void + removeCleared() + { + for (int i = registrants.size() - 1; i >= 0 ; i--) { + Registrant r = (Registrant) registrants.get(i); + + if (r.refH == null) { + registrants.remove(i); + } + } + } + + public synchronized int + size() + { + return registrants.size(); + } + + public synchronized Object + get(int index) + { + return registrants.get(index); + } + + private synchronized void + internalNotifyRegistrants (Object result, Throwable exception) + { + for (int i = 0, s = registrants.size(); i < s ; i++) { + Registrant r = (Registrant) registrants.get(i); + r.internalNotifyRegistrant(result, exception); + } + } + + public /*synchronized*/ void + notifyRegistrants() + { + internalNotifyRegistrants(null, null); + } + + public /*synchronized*/ void + notifyException(Throwable exception) + { + internalNotifyRegistrants (null, exception); + } + + public /*synchronized*/ void + notifyResult(Object result) + { + internalNotifyRegistrants (result, null); + } + + + public /*synchronized*/ void + notifyRegistrants(AsyncResult ar) + { + internalNotifyRegistrants(ar.result, ar.exception); + } + + public synchronized void + remove(Handler h) + { + for (int i = 0, s = registrants.size() ; i < s ; i++) { + Registrant r = (Registrant) registrants.get(i); + Handler rh; + + rh = r.getHandler(); + + /* Clean up both the requested registrant and + * any now-collected registrants + */ + if (rh == null || rh == h) { + r.clear(); + } + } + + removeCleared(); + } +} diff --git a/src/main/java/android/os/RemoteCallback.java b/src/main/java/android/os/RemoteCallback.java new file mode 100644 index 0000000..ca95bdf --- /dev/null +++ b/src/main/java/android/os/RemoteCallback.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010 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 android.os; + +/** + * TODO: Make this a public API? Let's see how it goes with a few use + * cases first. + * @hide + */ +public abstract class RemoteCallback implements Parcelable { + final Handler mHandler; + final IRemoteCallback mTarget; + + class DeliverResult implements Runnable { + final Bundle mResult; + + DeliverResult(Bundle result) { + mResult = result; + } + + public void run() { + onResult(mResult); + } + } + + class LocalCallback extends IRemoteCallback.Stub { + public void sendResult(Bundle bundle) { + mHandler.post(new DeliverResult(bundle)); + } + } + + static class RemoteCallbackProxy extends RemoteCallback { + RemoteCallbackProxy(IRemoteCallback target) { + super(target); + } + + protected void onResult(Bundle bundle) { + } + } + + public RemoteCallback(Handler handler) { + mHandler = handler; + mTarget = new LocalCallback(); + } + + RemoteCallback(IRemoteCallback target) { + mHandler = null; + mTarget = target; + } + + public void sendResult(Bundle bundle) throws RemoteException { + mTarget.sendResult(bundle); + } + + protected abstract void onResult(Bundle bundle); + + public boolean equals(Object otherObj) { + if (otherObj == null) { + return false; + } + try { + return mTarget.asBinder().equals(((RemoteCallback)otherObj) + .mTarget.asBinder()); + } catch (ClassCastException e) { + } + return false; + } + + public int hashCode() { + return mTarget.asBinder().hashCode(); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeStrongBinder(mTarget.asBinder()); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public RemoteCallback createFromParcel(Parcel in) { + IBinder target = in.readStrongBinder(); + return target != null ? new RemoteCallbackProxy( + IRemoteCallback.Stub.asInterface(target)) : null; + } + + public RemoteCallback[] newArray(int size) { + return new RemoteCallback[size]; + } + }; +} diff --git a/src/main/java/android/os/RemoteCallbackList.java b/src/main/java/android/os/RemoteCallbackList.java new file mode 100644 index 0000000..d2a9cdc --- /dev/null +++ b/src/main/java/android/os/RemoteCallbackList.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2008 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 android.os; + +import android.util.ArrayMap; + +/** + * Takes care of the grunt work of maintaining a list of remote interfaces, + * typically for the use of performing callbacks from a + * {@link android.app.Service} to its clients. In particular, this: + * + *

      + *
    • Keeps track of a set of registered {@link IInterface} callbacks, + * taking care to identify them through their underlying unique {@link IBinder} + * (by calling {@link IInterface#asBinder IInterface.asBinder()}. + *
    • Attaches a {@link IBinder.DeathRecipient IBinder.DeathRecipient} to + * each registered interface, so that it can be cleaned out of the list if its + * process goes away. + *
    • Performs locking of the underlying list of interfaces to deal with + * multithreaded incoming calls, and a thread-safe way to iterate over a + * snapshot of the list without holding its lock. + *
    + * + *

    To use this class, simply create a single instance along with your + * service, and call its {@link #register} and {@link #unregister} methods + * as client register and unregister with your service. To call back on to + * the registered clients, use {@link #beginBroadcast}, + * {@link #getBroadcastItem}, and {@link #finishBroadcast}. + * + *

    If a registered callback's process goes away, this class will take + * care of automatically removing it from the list. If you want to do + * additional work in this situation, you can create a subclass that + * implements the {@link #onCallbackDied} method. + */ +public class RemoteCallbackList { + /*package*/ ArrayMap mCallbacks + = new ArrayMap(); + private Object[] mActiveBroadcast; + private int mBroadcastCount = -1; + private boolean mKilled = false; + + private final class Callback implements IBinder.DeathRecipient { + final E mCallback; + final Object mCookie; + + Callback(E callback, Object cookie) { + mCallback = callback; + mCookie = cookie; + } + + public void binderDied() { + synchronized (mCallbacks) { + mCallbacks.remove(mCallback.asBinder()); + } + onCallbackDied(mCallback, mCookie); + } + } + + /** + * Simple version of {@link RemoteCallbackList#register(E, Object)} + * that does not take a cookie object. + */ + public boolean register(E callback) { + return register(callback, null); + } + + /** + * Add a new callback to the list. This callback will remain in the list + * until a corresponding call to {@link #unregister} or its hosting process + * goes away. If the callback was already registered (determined by + * checking to see if the {@link IInterface#asBinder callback.asBinder()} + * object is already in the list), then it will be left as-is. + * Registrations are not counted; a single call to {@link #unregister} + * will remove a callback after any number calls to register it. + * + * @param callback The callback interface to be added to the list. Must + * not be null -- passing null here will cause a NullPointerException. + * Most services will want to check for null before calling this with + * an object given from a client, so that clients can't crash the + * service with bad data. + * + * @param cookie Optional additional data to be associated with this + * callback. + * + * @return Returns true if the callback was successfully added to the list. + * Returns false if it was not added, either because {@link #kill} had + * previously been called or the callback's process has gone away. + * + * @see #unregister + * @see #kill + * @see #onCallbackDied + */ + public boolean register(E callback, Object cookie) { + synchronized (mCallbacks) { + if (mKilled) { + return false; + } + IBinder binder = callback.asBinder(); + try { + Callback cb = new Callback(callback, cookie); + binder.linkToDeath(cb, 0); + mCallbacks.put(binder, cb); + return true; + } catch (RemoteException e) { + return false; + } + } + } + + /** + * Remove from the list a callback that was previously added with + * {@link #register}. This uses the + * {@link IInterface#asBinder callback.asBinder()} object to correctly + * find the previous registration. + * Registrations are not counted; a single unregister call will remove + * a callback after any number calls to {@link #register} for it. + * + * @param callback The callback to be removed from the list. Passing + * null here will cause a NullPointerException, so you will generally want + * to check for null before calling. + * + * @return Returns true if the callback was found and unregistered. Returns + * false if the given callback was not found on the list. + * + * @see #register + */ + public boolean unregister(E callback) { + synchronized (mCallbacks) { + Callback cb = mCallbacks.remove(callback.asBinder()); + if (cb != null) { + cb.mCallback.asBinder().unlinkToDeath(cb, 0); + return true; + } + return false; + } + } + + /** + * Disable this callback list. All registered callbacks are unregistered, + * and the list is disabled so that future calls to {@link #register} will + * fail. This should be used when a Service is stopping, to prevent clients + * from registering callbacks after it is stopped. + * + * @see #register + */ + public void kill() { + synchronized (mCallbacks) { + for (int cbi=mCallbacks.size()-1; cbi>=0; cbi--) { + Callback cb = mCallbacks.valueAt(cbi); + cb.mCallback.asBinder().unlinkToDeath(cb, 0); + } + mCallbacks.clear(); + mKilled = true; + } + } + + /** + * Old version of {@link #onCallbackDied(E, Object)} that + * does not provide a cookie. + */ + public void onCallbackDied(E callback) { + } + + /** + * Called when the process hosting a callback in the list has gone away. + * The default implementation calls {@link #onCallbackDied(E)} + * for backwards compatibility. + * + * @param callback The callback whose process has died. Note that, since + * its process has died, you can not make any calls on to this interface. + * You can, however, retrieve its IBinder and compare it with another + * IBinder to see if it is the same object. + * @param cookie The cookie object original provided to + * {@link #register(E, Object)}. + * + * @see #register + */ + public void onCallbackDied(E callback, Object cookie) { + onCallbackDied(callback); + } + + /** + * Prepare to start making calls to the currently registered callbacks. + * This creates a copy of the callback list, which you can retrieve items + * from using {@link #getBroadcastItem}. Note that only one broadcast can + * be active at a time, so you must be sure to always call this from the + * same thread (usually by scheduling with {@link Handler}) or + * do your own synchronization. You must call {@link #finishBroadcast} + * when done. + * + *

    A typical loop delivering a broadcast looks like this: + * + *

    +     * int i = callbacks.beginBroadcast();
    +     * while (i > 0) {
    +     *     i--;
    +     *     try {
    +     *         callbacks.getBroadcastItem(i).somethingHappened();
    +     *     } catch (RemoteException e) {
    +     *         // The RemoteCallbackList will take care of removing
    +     *         // the dead object for us.
    +     *     }
    +     * }
    +     * callbacks.finishBroadcast();
    + * + * @return Returns the number of callbacks in the broadcast, to be used + * with {@link #getBroadcastItem} to determine the range of indices you + * can supply. + * + * @see #getBroadcastItem + * @see #finishBroadcast + */ + public int beginBroadcast() { + synchronized (mCallbacks) { + if (mBroadcastCount > 0) { + throw new IllegalStateException( + "beginBroadcast() called while already in a broadcast"); + } + + final int N = mBroadcastCount = mCallbacks.size(); + if (N <= 0) { + return 0; + } + Object[] active = mActiveBroadcast; + if (active == null || active.length < N) { + mActiveBroadcast = active = new Object[N]; + } + for (int i=0; ionly be called after + * the broadcast is started, and its data is no longer valid after + * calling {@link #finishBroadcast}. + * + *

    Note that it is possible for the process of one of the returned + * callbacks to go away before you call it, so you will need to catch + * {@link RemoteException} when calling on to the returned object. + * The callback list itself, however, will take care of unregistering + * these objects once it detects that it is no longer valid, so you can + * handle such an exception by simply ignoring it. + * + * @param index Which of the registered callbacks you would like to + * retrieve. Ranges from 0 to 1-{@link #beginBroadcast}. + * + * @return Returns the callback interface that you can call. This will + * always be non-null. + * + * @see #beginBroadcast + */ + public E getBroadcastItem(int index) { + return ((Callback)mActiveBroadcast[index]).mCallback; + } + + /** + * Retrieve the cookie associated with the item + * returned by {@link #getBroadcastItem(int)}. + * + * @see #getBroadcastItem + */ + public Object getBroadcastCookie(int index) { + return ((Callback)mActiveBroadcast[index]).mCookie; + } + + /** + * Clean up the state of a broadcast previously initiated by calling + * {@link #beginBroadcast}. This must always be called when you are done + * with a broadcast. + * + * @see #beginBroadcast + */ + public void finishBroadcast() { + if (mBroadcastCount < 0) { + throw new IllegalStateException( + "finishBroadcast() called outside of a broadcast"); + } + + Object[] active = mActiveBroadcast; + if (active != null) { + final int N = mBroadcastCount; + for (int i=0; i + * This function is useful to decide whether to schedule a broadcast if this + * requires doing some work which otherwise would not be performed. + *

    + * + * @return The size. + */ + public int getRegisteredCallbackCount() { + synchronized (mCallbacks) { + if (mKilled) { + return 0; + } + return mCallbacks.size(); + } + } +} diff --git a/src/main/java/android/os/RemoteException.java b/src/main/java/android/os/RemoteException.java new file mode 100644 index 0000000..98d7523 --- /dev/null +++ b/src/main/java/android/os/RemoteException.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2006 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 android.os; +import android.util.AndroidException; + +/** + * Parent exception for all Binder remote-invocation errors + */ +public class RemoteException extends AndroidException { + public RemoteException() { + super(); + } + + public RemoteException(String message) { + super(message); + } + + /** {@hide} */ + public RuntimeException rethrowAsRuntimeException() { + throw new RuntimeException(this); + } +} diff --git a/src/main/java/android/os/RemoteMailException.java b/src/main/java/android/os/RemoteMailException.java new file mode 100644 index 0000000..1ac96d1 --- /dev/null +++ b/src/main/java/android/os/RemoteMailException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2006 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 android.os; + +/** @hide */ +public class RemoteMailException extends Exception +{ + public RemoteMailException() + { + } + + public RemoteMailException(String s) + { + super(s); + } +} + diff --git a/src/main/java/android/os/ResultReceiver.java b/src/main/java/android/os/ResultReceiver.java new file mode 100644 index 0000000..34a66b6 --- /dev/null +++ b/src/main/java/android/os/ResultReceiver.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2009 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 android.os; + +import com.android.internal.os.IResultReceiver; + +/** + * Generic interface for receiving a callback result from someone. Use this + * by creating a subclass and implement {@link #onReceiveResult}, which you can + * then pass to others and send through IPC, and receive results they + * supply with {@link #send}. + * + *

    Note: the implementation underneath is just a simple wrapper around + * a {@link Binder} that is used to perform the communication. This means + * semantically you should treat it as such: this class does not impact process + * lifecycle management (you must be using some higher-level component to tell + * the system that your process needs to continue running), the connection will + * break if your process goes away for any reason, etc.

    + */ +public class ResultReceiver implements Parcelable { + final boolean mLocal; + final Handler mHandler; + + IResultReceiver mReceiver; + + class MyRunnable implements Runnable { + final int mResultCode; + final Bundle mResultData; + + MyRunnable(int resultCode, Bundle resultData) { + mResultCode = resultCode; + mResultData = resultData; + } + + public void run() { + onReceiveResult(mResultCode, mResultData); + } + } + + class MyResultReceiver extends IResultReceiver.Stub { + public void send(int resultCode, Bundle resultData) { + if (mHandler != null) { + mHandler.post(new MyRunnable(resultCode, resultData)); + } else { + onReceiveResult(resultCode, resultData); + } + } + } + + /** + * Create a new ResultReceive to receive results. Your + * {@link #onReceiveResult} method will be called from the thread running + * handler if given, or from an arbitrary thread if null. + */ + public ResultReceiver(Handler handler) { + mLocal = true; + mHandler = handler; + } + + /** + * Deliver a result to this receiver. Will call {@link #onReceiveResult}, + * always asynchronously if the receiver has supplied a Handler in which + * to dispatch the result. + * @param resultCode Arbitrary result code to deliver, as defined by you. + * @param resultData Any additional data provided by you. + */ + public void send(int resultCode, Bundle resultData) { + if (mLocal) { + if (mHandler != null) { + mHandler.post(new MyRunnable(resultCode, resultData)); + } else { + onReceiveResult(resultCode, resultData); + } + return; + } + + if (mReceiver != null) { + try { + mReceiver.send(resultCode, resultData); + } catch (RemoteException e) { + } + } + } + + /** + * Override to receive results delivered to this object. + * + * @param resultCode Arbitrary result code delivered by the sender, as + * defined by the sender. + * @param resultData Any additional data provided by the sender. + */ + protected void onReceiveResult(int resultCode, Bundle resultData) { + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + synchronized (this) { + if (mReceiver == null) { + mReceiver = new MyResultReceiver(); + } + out.writeStrongBinder(mReceiver.asBinder()); + } + } + + ResultReceiver(Parcel in) { + mLocal = false; + mHandler = null; + mReceiver = IResultReceiver.Stub.asInterface(in.readStrongBinder()); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public ResultReceiver createFromParcel(Parcel in) { + return new ResultReceiver(in); + } + public ResultReceiver[] newArray(int size) { + return new ResultReceiver[size]; + } + }; +} diff --git a/src/main/java/android/os/SELinux.java b/src/main/java/android/os/SELinux.java new file mode 100644 index 0000000..84aa427 --- /dev/null +++ b/src/main/java/android/os/SELinux.java @@ -0,0 +1,198 @@ +/* + * 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 android.os; + +import android.util.Slog; + +import java.io.IOException; +import java.io.File; +import java.io.FileDescriptor; + +/** + * This class provides access to the centralized jni bindings for + * SELinux interaction. + * {@hide} + */ +public class SELinux { + private static final String TAG = "SELinux"; + + /** Keep in sync with ./external/libselinux/include/selinux/android.h */ + private static final int SELINUX_ANDROID_RESTORECON_NOCHANGE = 1; + private static final int SELINUX_ANDROID_RESTORECON_VERBOSE = 2; + private static final int SELINUX_ANDROID_RESTORECON_RECURSE = 4; + private static final int SELINUX_ANDROID_RESTORECON_FORCE = 8; + private static final int SELINUX_ANDROID_RESTORECON_DATADATA = 16; + + /** + * Determine whether SELinux is disabled or enabled. + * @return a boolean indicating whether SELinux is enabled. + */ + public static final native boolean isSELinuxEnabled(); + + /** + * Determine whether SELinux is permissive or enforcing. + * @return a boolean indicating whether SELinux is enforcing. + */ + public static final native boolean isSELinuxEnforced(); + + /** + * Set whether SELinux is permissive or enforcing. + * @param value representing whether to set SELinux to enforcing + * @return a boolean representing whether the desired mode was set + */ + public static final native boolean setSELinuxEnforce(boolean value); + + /** + * Sets the security context for newly created file objects. + * @param context a security context given as a String. + * @return a boolean indicating whether the operation succeeded. + */ + public static final native boolean setFSCreateContext(String context); + + /** + * Change the security context of an existing file object. + * @param path representing the path of file object to relabel. + * @param context new security context given as a String. + * @return a boolean indicating whether the operation succeeded. + */ + public static final native boolean setFileContext(String path, String context); + + /** + * Get the security context of a file object. + * @param path the pathname of the file object. + * @return a security context given as a String. + */ + public static final native String getFileContext(String path); + + /** + * Get the security context of a peer socket. + * @param fd FileDescriptor class of the peer socket. + * @return a String representing the peer socket security context. + */ + public static final native String getPeerContext(FileDescriptor fd); + + /** + * Gets the security context of the current process. + * @return a String representing the security context of the current process. + */ + public static final native String getContext(); + + /** + * Gets the security context of a given process id. + * @param pid an int representing the process id to check. + * @return a String representing the security context of the given pid. + */ + public static final native String getPidContext(int pid); + + /** + * Gets a list of the SELinux boolean names. + * @return an array of strings containing the SELinux boolean names. + */ + public static final native String[] getBooleanNames(); + + /** + * Gets the value for the given SELinux boolean name. + * @param name The name of the SELinux boolean. + * @return a boolean indicating whether the SELinux boolean is set. + */ + public static final native boolean getBooleanValue(String name); + + /** + * Sets the value for the given SELinux boolean name. + * @param name The name of the SELinux boolean. + * @param value The new value of the SELinux boolean. + * @return a boolean indicating whether or not the operation succeeded. + */ + public static final native boolean setBooleanValue(String name, boolean value); + + /** + * Check permissions between two security contexts. + * @param scon The source or subject security context. + * @param tcon The target or object security context. + * @param tclass The object security class name. + * @param perm The permission name. + * @return a boolean indicating whether permission was granted. + */ + public static final native boolean checkSELinuxAccess(String scon, String tcon, String tclass, String perm); + + /** + * Restores a file to its default SELinux security context. + * If the system is not compiled with SELinux, then {@code true} + * is automatically returned. + * If SELinux is compiled in, but disabled, then {@code true} is + * returned. + * + * @param pathname The pathname of the file to be relabeled. + * @return a boolean indicating whether the relabeling succeeded. + * @exception NullPointerException if the pathname is a null object. + */ + public static boolean restorecon(String pathname) throws NullPointerException { + if (pathname == null) { throw new NullPointerException(); } + return native_restorecon(pathname, 0); + } + + /** + * Restores a file to its default SELinux security context. + * If the system is not compiled with SELinux, then {@code true} + * is automatically returned. + * If SELinux is compiled in, but disabled, then {@code true} is + * returned. + * + * @param pathname The pathname of the file to be relabeled. + * @return a boolean indicating whether the relabeling succeeded. + */ + private static native boolean native_restorecon(String pathname, int flags); + + /** + * Restores a file to its default SELinux security context. + * If the system is not compiled with SELinux, then {@code true} + * is automatically returned. + * If SELinux is compiled in, but disabled, then {@code true} is + * returned. + * + * @param file The File object representing the path to be relabeled. + * @return a boolean indicating whether the relabeling succeeded. + * @exception NullPointerException if the file is a null object. + */ + public static boolean restorecon(File file) throws NullPointerException { + try { + return native_restorecon(file.getCanonicalPath(), 0); + } catch (IOException e) { + Slog.e(TAG, "Error getting canonical path. Restorecon failed for " + + file.getPath(), e); + return false; + } + } + + /** + * Recursively restores all files under the given path to their default + * SELinux security context. If the system is not compiled with SELinux, + * then {@code true} is automatically returned. If SELinux is compiled in, + * but disabled, then {@code true} is returned. + * + * @return a boolean indicating whether the relabeling succeeded. + */ + public static boolean restoreconRecursive(File file) { + try { + return native_restorecon(file.getCanonicalPath(), SELINUX_ANDROID_RESTORECON_RECURSE); + } catch (IOException e) { + Slog.e(TAG, "Error getting canonical path. Restorecon failed for " + + file.getPath(), e); + return false; + } + } +} diff --git a/src/main/java/android/os/SELinuxTest.java b/src/main/java/android/os/SELinuxTest.java new file mode 100644 index 0000000..9b63a6b --- /dev/null +++ b/src/main/java/android/os/SELinuxTest.java @@ -0,0 +1,45 @@ +package android.os; + +import android.os.Process; +import android.os.SELinux; +import android.test.AndroidTestCase; +import static junit.framework.Assert.assertEquals; + +public class SELinuxTest extends AndroidTestCase { + + public void testgetFileCon() { + if(SELinux.isSELinuxEnabled() == false) + return; + + String ctx = SELinux.getFileContext("/system/bin/toolbox"); + assertEquals(ctx, "u:object_r:system_file:s0"); + } + + public void testgetCon() { + if(SELinux.isSELinuxEnabled() == false) + return; + + String mycon = SELinux.getContext(); + assertEquals(mycon, "u:r:untrusted_app:s0:c33"); + } + + public void testgetPidCon() { + if(SELinux.isSELinuxEnabled() == false) + return; + + String mycon = SELinux.getPidContext(Process.myPid()); + assertEquals(mycon, "u:r:untrusted_app:s0:c33"); + } + + public void testcheckSELinuxAccess() { + if(SELinux.isSELinuxEnabled() == false) + return; + + String mycon = SELinux.getContext(); + boolean ret; + ret = SELinux.checkSELinuxAccess(mycon, mycon, "process", "fork"); + assertEquals(ret,"true"); + ret = SELinux.checkSELinuxAccess(mycon, mycon, "memprotect", "mmap_zero"); + assertEquals(ret,"true"); + } +} diff --git a/src/main/java/android/os/ServiceManager.java b/src/main/java/android/os/ServiceManager.java new file mode 100644 index 0000000..6a68ee2 --- /dev/null +++ b/src/main/java/android/os/ServiceManager.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2009 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 android.os; + +import java.util.Map; + +public final class ServiceManager { + + /** + * Returns a reference to a service with the given name. + * + * @param name the name of the service to get + * @return a reference to the service, or null if the service doesn't exist + */ + public static IBinder getService(String name) { + return null; + } + + /** + * Place a new @a service called @a name into the service + * manager. + * + * @param name the name of the new service + * @param service the service object + */ + public static void addService(String name, IBinder service) { + // pass + } + + /** + * Retrieve an existing service called @a name from the + * service manager. Non-blocking. + */ + public static IBinder checkService(String name) { + return null; + } + + /** + * Return a list of all currently running services. + */ + public static String[] listServices() throws RemoteException { + // actual implementation returns null sometimes, so it's ok + // to return null instead of an empty list. + return null; + } + + /** + * This is only intended to be called when the process is first being brought + * up and bound by the activity manager. There is only one thread in the process + * at that time, so no locking is done. + * + * @param cache the cache of service references + * @hide + */ + public static void initServiceCache(Map cache) { + // pass + } +} diff --git a/src/main/java/android/os/ServiceManagerNative.java b/src/main/java/android/os/ServiceManagerNative.java new file mode 100644 index 0000000..be24426 --- /dev/null +++ b/src/main/java/android/os/ServiceManagerNative.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2006 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 android.os; + +import java.util.ArrayList; + + +/** + * Native implementation of the service manager. Most clients will only + * care about getDefault() and possibly asInterface(). + * @hide + */ +public abstract class ServiceManagerNative extends Binder implements IServiceManager +{ + /** + * Cast a Binder object into a service manager interface, generating + * a proxy if needed. + */ + static public IServiceManager asInterface(IBinder obj) + { + if (obj == null) { + return null; + } + IServiceManager in = + (IServiceManager)obj.queryLocalInterface(descriptor); + if (in != null) { + return in; + } + + return new ServiceManagerProxy(obj); + } + + public ServiceManagerNative() + { + attachInterface(this, descriptor); + } + + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + { + try { + switch (code) { + case IServiceManager.GET_SERVICE_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + String name = data.readString(); + IBinder service = getService(name); + reply.writeStrongBinder(service); + return true; + } + + case IServiceManager.CHECK_SERVICE_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + String name = data.readString(); + IBinder service = checkService(name); + reply.writeStrongBinder(service); + return true; + } + + case IServiceManager.ADD_SERVICE_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + String name = data.readString(); + IBinder service = data.readStrongBinder(); + boolean allowIsolated = data.readInt() != 0; + addService(name, service, allowIsolated); + return true; + } + + case IServiceManager.LIST_SERVICES_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + String[] list = listServices(); + reply.writeStringArray(list); + return true; + } + + case IServiceManager.SET_PERMISSION_CONTROLLER_TRANSACTION: { + data.enforceInterface(IServiceManager.descriptor); + IPermissionController controller + = IPermissionController.Stub.asInterface( + data.readStrongBinder()); + setPermissionController(controller); + return true; + } + } + } catch (RemoteException e) { + } + + return false; + } + + public IBinder asBinder() + { + return this; + } +} + +class ServiceManagerProxy implements IServiceManager { + public ServiceManagerProxy(IBinder remote) { + mRemote = remote; + } + + public IBinder asBinder() { + return mRemote; + } + + public IBinder getService(String name) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IServiceManager.descriptor); + data.writeString(name); + mRemote.transact(GET_SERVICE_TRANSACTION, data, reply, 0); + IBinder binder = reply.readStrongBinder(); + reply.recycle(); + data.recycle(); + return binder; + } + + public IBinder checkService(String name) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IServiceManager.descriptor); + data.writeString(name); + mRemote.transact(CHECK_SERVICE_TRANSACTION, data, reply, 0); + IBinder binder = reply.readStrongBinder(); + reply.recycle(); + data.recycle(); + return binder; + } + + public void addService(String name, IBinder service, boolean allowIsolated) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IServiceManager.descriptor); + data.writeString(name); + data.writeStrongBinder(service); + data.writeInt(allowIsolated ? 1 : 0); + mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0); + reply.recycle(); + data.recycle(); + } + + public String[] listServices() throws RemoteException { + ArrayList services = new ArrayList(); + int n = 0; + while (true) { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IServiceManager.descriptor); + data.writeInt(n); + n++; + try { + boolean res = mRemote.transact(LIST_SERVICES_TRANSACTION, data, reply, 0); + if (!res) { + break; + } + } catch (RuntimeException e) { + // The result code that is returned by the C++ code can + // cause the call to throw an exception back instead of + // returning a nice result... so eat it here and go on. + break; + } + services.add(reply.readString()); + reply.recycle(); + data.recycle(); + } + String[] array = new String[services.size()]; + services.toArray(array); + return array; + } + + public void setPermissionController(IPermissionController controller) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IServiceManager.descriptor); + data.writeStrongBinder(controller.asBinder()); + mRemote.transact(SET_PERMISSION_CONTROLLER_TRANSACTION, data, reply, 0); + reply.recycle(); + data.recycle(); + } + + private IBinder mRemote; +} diff --git a/src/main/java/android/os/SparseRectFArrayTest.java b/src/main/java/android/os/SparseRectFArrayTest.java new file mode 100644 index 0000000..47b0d2a --- /dev/null +++ b/src/main/java/android/os/SparseRectFArrayTest.java @@ -0,0 +1,272 @@ +/* + * 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 android.os; + +import android.graphics.RectF; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.inputmethod.SparseRectFArray; +import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder; + +import java.util.Objects; + +public class SparseRectFArrayTest extends InstrumentationTestCase { + // A test data for {@link SparseRectFArray}. null represents the gap of indices. + private static final RectF[] MANY_RECTS = new RectF[] { + null, + new RectF(102.0f, 202.0f, 302.0f, 402.0f), + new RectF(103.0f, 203.0f, 303.0f, 403.0f), + new RectF(104.0f, 204.0f, 304.0f, 404.0f), + new RectF(105.0f, 205.0f, 305.0f, 405.0f), + new RectF(106.0f, 206.0f, 306.0f, 406.0f), + null, + new RectF(108.0f, 208.0f, 308.0f, 408.0f), + new RectF(109.0f, 209.0f, 309.0f, 409.0f), + new RectF(110.0f, 210.0f, 310.0f, 410.0f), + new RectF(111.0f, 211.0f, 311.0f, 411.0f), + new RectF(112.0f, 212.0f, 312.0f, 412.0f), + new RectF(113.0f, 213.0f, 313.0f, 413.0f), + new RectF(114.0f, 214.0f, 314.0f, 414.0f), + new RectF(115.0f, 215.0f, 315.0f, 415.0f), + new RectF(116.0f, 216.0f, 316.0f, 416.0f), + new RectF(117.0f, 217.0f, 317.0f, 417.0f), + null, + null, + new RectF(118.0f, 218.0f, 318.0f, 418.0f), + }; + + @SmallTest + public void testBuilder() throws Exception { + final RectF TEMP_RECT = new RectF(10.0f, 20.0f, 30.0f, 40.0f); + final int TEMP_FLAGS = 0x1234; + + final SparseRectFArrayBuilder builder = new SparseRectFArrayBuilder(); + builder.append(100, TEMP_RECT.left, TEMP_RECT.top, TEMP_RECT.right, TEMP_RECT.bottom, + TEMP_FLAGS); + assertNull(builder.build().get(-1)); + assertNull(builder.build().get(0)); + assertNull(builder.build().get(99)); + assertEquals(0, builder.build().getFlags(99, 0 /* valueIfKeyNotFound */)); + assertEquals(1, builder.build().getFlags(99, 1 /* valueIfKeyNotFound */)); + assertEquals(TEMP_RECT, builder.build().get(100)); + assertEquals(TEMP_FLAGS, builder.build().getFlags(100, 0 /* valueIfKeyNotFound */)); + assertEquals(TEMP_FLAGS, builder.build().getFlags(100, 1 /* valueIfKeyNotFound */)); + assertNull(builder.build().get(101)); + assertEquals(0, builder.build().getFlags(101, 0 /* valueIfKeyNotFound */)); + assertEquals(1, builder.build().getFlags(101, 1 /* valueIfKeyNotFound */)); + + // Test if {@link SparseRectFArrayBuilder#reset} resets its internal state. + builder.reset(); + assertNull(builder.build().get(100)); + + builder.reset(); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF rect = MANY_RECTS[i]; + if (rect != null) { + builder.append(i, rect.left, rect.top, rect.right, rect.bottom, i); + } + } + final SparseRectFArray array = builder.build(); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF expectedRect = MANY_RECTS[i]; + assertEquals(expectedRect, array.get(i)); + if (expectedRect != null) { + assertEquals(i, array.getFlags(i, 0x1234 /* valueIfKeyNotFound */)); + assertEquals(i, array.getFlags(i, 0x4321 /* valueIfKeyNotFound */)); + } else { + assertEquals(0x1234, array.getFlags(i, 0x1234 /* valueIfKeyNotFound */)); + assertEquals(0x4321, array.getFlags(i, 0x4321 /* valueIfKeyNotFound */)); + } + } + + // Make sure the builder reproduces an equivalent object. + final SparseRectFArray array2 = builder.build(); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF expectedRect = MANY_RECTS[i]; + assertEquals(expectedRect, array2.get(i)); + if (expectedRect != null) { + assertEquals(i, array2.getFlags(i, 0x1234 /* valueIfKeyNotFound */)); + assertEquals(i, array2.getFlags(i, 0x4321 /* valueIfKeyNotFound */)); + } else { + assertEquals(0x1234, array2.getFlags(i, 0x1234 /* valueIfKeyNotFound */)); + assertEquals(0x4321, array2.getFlags(i, 0x4321 /* valueIfKeyNotFound */)); + } + } + assertEqualRects(array, array2); + + // Make sure the instance can be marshaled via {@link Parcel}. + final SparseRectFArray array3 = cloneViaParcel(array); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF expectedRect = MANY_RECTS[i]; + assertEquals(expectedRect, array3.get(i)); + if (expectedRect != null) { + assertEquals(i, array3.getFlags(i, 0x1234 /* valueIfKeyNotFound */)); + assertEquals(i, array3.getFlags(i, 0x4321 /* valueIfKeyNotFound */)); + } else { + assertEquals(0x1234, array3.getFlags(i, 0x1234 /* valueIfKeyNotFound */)); + assertEquals(0x4321, array3.getFlags(i, 0x4321 /* valueIfKeyNotFound */)); + } + } + assertEqualRects(array, array3); + + // Make sure the builder can be reset. + builder.reset(); + assertNull(builder.build().get(0)); + } + + @SmallTest + public void testEquality() throws Exception { + // Empty array should be equal. + assertEqualRects(new SparseRectFArrayBuilder().build(), + new SparseRectFArrayBuilder().build()); + + assertEqualRects( + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f, 1).build(), + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f, 1).build()); + assertEqualRects( + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0).build(), + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0).build(), + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f, 1).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f, 1).build(), + new SparseRectFArrayBuilder().append(100, 2.0f, 2.0f, 3.0f, 4.0f, 1).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f, 1).build(), + new SparseRectFArrayBuilder().append(101, 1.0f, 2.0f, 3.0f, 4.0f, 1).build()); + + assertEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f, 0).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f, 0).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f, 0).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f, 0).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f, 0).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(101, 1.0f, 0.0f, 0.0f, 0.0f, 0).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(101, 1.0f, 0.0f, 0.0f, 0.0f, 0).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f, 0).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f, 0).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(102, 0.0f, 0.0f, 0.0f, 0.0f, 0).build()); + + assertEqualRects( + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(1000, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .append(100000000, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .build(), + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(1000, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .append(100000000, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .build()); + + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(1000, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .append(100000000, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .build(), + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(1000, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .append(100000000, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .build(), + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f, 0) + .append(1000, 1.0f, 0.0f, 0.0f, 0.0f, 0) + .append(100000000, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .build()); + } + + @SmallTest + public void testBuilderAppend() throws Exception { + // Key should be appended in ascending order. + try { + new SparseRectFArrayBuilder() + .append(10, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .append(0, 1.0f, 2.0f, 3.0f, 4.0f, 0); + } catch (IllegalArgumentException ex) { + assertTrue(true); + } + + try { + new SparseRectFArrayBuilder() + .append(10, 0.0f, 0.0f, 0.0f, 0.0f, 0) + .append(10, 1.0f, 2.0f, 3.0f, 4.0f, 0); + } catch (IllegalArgumentException ex) { + assertTrue(true); + } + } + + private static void assertEqualRects(SparseRectFArray a, SparseRectFArray b) { + assertEquals(a, b); + if (a != null && b != null) { + assertEquals(a.hashCode(), b.hashCode()); + } + } + + private static void assertNotEqualRects(SparseRectFArray a, SparseRectFArray b) { + assertFalse(Objects.equals(a, b)); + } + + private static SparseRectFArray cloneViaParcel(final SparseRectFArray src) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + src.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return new SparseRectFArray(parcel); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } +} diff --git a/src/main/java/android/os/StatFs.java b/src/main/java/android/os/StatFs.java new file mode 100644 index 0000000..13e9a15 --- /dev/null +++ b/src/main/java/android/os/StatFs.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructStatVfs; + +/** + * Retrieve overall information about the space on a filesystem. This is a + * wrapper for Unix statvfs(). + */ +public class StatFs { + private StructStatVfs mStat; + + /** + * Construct a new StatFs for looking at the stats of the filesystem at + * {@code path}. Upon construction, the stat of the file system will be + * performed, and the values retrieved available from the methods on this + * class. + * + * @param path path in the desired file system to stat. + */ + public StatFs(String path) { + mStat = doStat(path); + } + + private static StructStatVfs doStat(String path) { + try { + return Os.statvfs(path); + } catch (ErrnoException e) { + throw new IllegalArgumentException("Invalid path: " + path, e); + } + } + + /** + * Perform a restat of the file system referenced by this object. This is + * the same as re-constructing the object with the same file system path, + * and the new stat values are available upon return. + */ + public void restat(String path) { + mStat = doStat(path); + } + + /** + * @deprecated Use {@link #getBlockSizeLong()} instead. + */ + @Deprecated + public int getBlockSize() { + return (int) mStat.f_bsize; + } + + /** + * The size, in bytes, of a block on the file system. This corresponds to + * the Unix {@code statvfs.f_bsize} field. + */ + public long getBlockSizeLong() { + return mStat.f_bsize; + } + + /** + * @deprecated Use {@link #getBlockCountLong()} instead. + */ + @Deprecated + public int getBlockCount() { + return (int) mStat.f_blocks; + } + + /** + * The total number of blocks on the file system. This corresponds to the + * Unix {@code statvfs.f_blocks} field. + */ + public long getBlockCountLong() { + return mStat.f_blocks; + } + + /** + * @deprecated Use {@link #getFreeBlocksLong()} instead. + */ + @Deprecated + public int getFreeBlocks() { + return (int) mStat.f_bfree; + } + + /** + * The total number of blocks that are free on the file system, including + * reserved blocks (that are not available to normal applications). This + * corresponds to the Unix {@code statvfs.f_bfree} field. Most applications + * will want to use {@link #getAvailableBlocks()} instead. + */ + public long getFreeBlocksLong() { + return mStat.f_bfree; + } + + /** + * The number of bytes that are free on the file system, including reserved + * blocks (that are not available to normal applications). Most applications + * will want to use {@link #getAvailableBytes()} instead. + */ + public long getFreeBytes() { + return mStat.f_bfree * mStat.f_bsize; + } + + /** + * @deprecated Use {@link #getAvailableBlocksLong()} instead. + */ + @Deprecated + public int getAvailableBlocks() { + return (int) mStat.f_bavail; + } + + /** + * The number of blocks that are free on the file system and available to + * applications. This corresponds to the Unix {@code statvfs.f_bavail} field. + */ + public long getAvailableBlocksLong() { + return mStat.f_bavail; + } + + /** + * The number of bytes that are free on the file system and available to + * applications. + */ + public long getAvailableBytes() { + return mStat.f_bavail * mStat.f_bsize; + } + + /** + * The total number of bytes supported by the file system. + */ + public long getTotalBytes() { + return mStat.f_blocks * mStat.f_bsize; + } +} diff --git a/src/main/java/android/os/StrictMode.java b/src/main/java/android/os/StrictMode.java new file mode 100644 index 0000000..6db5f67 --- /dev/null +++ b/src/main/java/android/os/StrictMode.java @@ -0,0 +1,2328 @@ +/* + * Copyright (C) 2010 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 android.os; + +import android.animation.ValueAnimator; +import android.app.ActivityManagerNative; +import android.app.ActivityThread; +import android.app.ApplicationErrorReport; +import android.app.IActivityManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Printer; +import android.util.Singleton; +import android.util.Slog; +import android.view.IWindowManager; + +import com.android.internal.os.RuntimeInit; + +import com.android.internal.util.FastPrintWriter; +import dalvik.system.BlockGuard; +import dalvik.system.CloseGuard; +import dalvik.system.VMDebug; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + *

    StrictMode is a developer tool which detects things you might be + * doing by accident and brings them to your attention so you can fix + * them. + * + *

    StrictMode is most commonly used to catch accidental disk or + * network access on the application's main thread, where UI + * operations are received and animations take place. Keeping disk + * and network operations off the main thread makes for much smoother, + * more responsive applications. By keeping your application's main thread + * responsive, you also prevent + * ANR dialogs + * from being shown to users. + * + *

    Note that even though an Android device's disk is + * often on flash memory, many devices run a filesystem on top of that + * memory with very limited concurrency. It's often the case that + * almost all disk accesses are fast, but may in individual cases be + * dramatically slower when certain I/O is happening in the background + * from other processes. If possible, it's best to assume that such + * things are not fast.

    + * + *

    Example code to enable from early in your + * {@link android.app.Application}, {@link android.app.Activity}, or + * other application component's + * {@link android.app.Application#onCreate} method: + * + *

    + * public void onCreate() {
    + *     if (DEVELOPER_MODE) {
    + *         StrictMode.setThreadPolicy(new {@link ThreadPolicy.Builder StrictMode.ThreadPolicy.Builder}()
    + *                 .detectDiskReads()
    + *                 .detectDiskWrites()
    + *                 .detectNetwork()   // or .detectAll() for all detectable problems
    + *                 .penaltyLog()
    + *                 .build());
    + *         StrictMode.setVmPolicy(new {@link VmPolicy.Builder StrictMode.VmPolicy.Builder}()
    + *                 .detectLeakedSqlLiteObjects()
    + *                 .detectLeakedClosableObjects()
    + *                 .penaltyLog()
    + *                 .penaltyDeath()
    + *                 .build());
    + *     }
    + *     super.onCreate();
    + * }
    + * 
    + * + *

    You can decide what should happen when a violation is detected. + * For example, using {@link ThreadPolicy.Builder#penaltyLog} you can + * watch the output of adb logcat while you use your + * application to see the violations as they happen. + * + *

    If you find violations that you feel are problematic, there are + * a variety of tools to help solve them: threads, {@link android.os.Handler}, + * {@link android.os.AsyncTask}, {@link android.app.IntentService}, etc. + * But don't feel compelled to fix everything that StrictMode finds. In particular, + * many cases of disk access are often necessary during the normal activity lifecycle. Use + * StrictMode to find things you did by accident. Network requests on the UI thread + * are almost always a problem, though. + * + *

    StrictMode is not a security mechanism and is not + * guaranteed to find all disk or network accesses. While it does + * propagate its state across process boundaries when doing + * {@link android.os.Binder} calls, it's still ultimately a best + * effort mechanism. Notably, disk or network access from JNI calls + * won't necessarily trigger it. Future versions of Android may catch + * more (or fewer) operations, so you should never leave StrictMode + * enabled in applications distributed on Google Play. + */ +public final class StrictMode { + private static final String TAG = "StrictMode"; + private static final boolean LOG_V = Log.isLoggable(TAG, Log.VERBOSE); + + private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE); + private static final boolean IS_ENG_BUILD = "eng".equals(Build.TYPE); + + /** + * Boolean system property to disable strict mode checks outright. + * Set this to 'true' to force disable; 'false' has no effect on other + * enable/disable policy. + * @hide + */ + public static final String DISABLE_PROPERTY = "persist.sys.strictmode.disable"; + + /** + * The boolean system property to control screen flashes on violations. + * + * @hide + */ + public static final String VISUAL_PROPERTY = "persist.sys.strictmode.visual"; + + // Only log a duplicate stack trace to the logs every second. + private static final long MIN_LOG_INTERVAL_MS = 1000; + + // Only show an annoying dialog at most every 30 seconds + private static final long MIN_DIALOG_INTERVAL_MS = 30000; + + // How many Span tags (e.g. animations) to report. + private static final int MAX_SPAN_TAGS = 20; + + // How many offending stacks to keep track of (and time) per loop + // of the Looper. + private static final int MAX_OFFENSES_PER_LOOP = 10; + + // Thread-policy: + + /** + * @hide + */ + public static final int DETECT_DISK_WRITE = 0x01; // for ThreadPolicy + + /** + * @hide + */ + public static final int DETECT_DISK_READ = 0x02; // for ThreadPolicy + + /** + * @hide + */ + public static final int DETECT_NETWORK = 0x04; // for ThreadPolicy + + /** + * For StrictMode.noteSlowCall() + * + * @hide + */ + public static final int DETECT_CUSTOM = 0x08; // for ThreadPolicy + + private static final int ALL_THREAD_DETECT_BITS = + DETECT_DISK_WRITE | DETECT_DISK_READ | DETECT_NETWORK | DETECT_CUSTOM; + + // Process-policy: + + /** + * Note, a "VM_" bit, not thread. + * @hide + */ + public static final int DETECT_VM_CURSOR_LEAKS = 0x200; // for VmPolicy + + /** + * Note, a "VM_" bit, not thread. + * @hide + */ + public static final int DETECT_VM_CLOSABLE_LEAKS = 0x400; // for VmPolicy + + /** + * Note, a "VM_" bit, not thread. + * @hide + */ + public static final int DETECT_VM_ACTIVITY_LEAKS = 0x800; // for VmPolicy + + /** + * @hide + */ + private static final int DETECT_VM_INSTANCE_LEAKS = 0x1000; // for VmPolicy + + /** + * @hide + */ + public static final int DETECT_VM_REGISTRATION_LEAKS = 0x2000; // for VmPolicy + + /** + * @hide + */ + private static final int DETECT_VM_FILE_URI_EXPOSURE = 0x4000; // for VmPolicy + + private static final int ALL_VM_DETECT_BITS = + DETECT_VM_CURSOR_LEAKS | DETECT_VM_CLOSABLE_LEAKS | + DETECT_VM_ACTIVITY_LEAKS | DETECT_VM_INSTANCE_LEAKS | + DETECT_VM_REGISTRATION_LEAKS | DETECT_VM_FILE_URI_EXPOSURE; + + /** + * @hide + */ + public static final int PENALTY_LOG = 0x10; // normal android.util.Log + + // Used for both process and thread policy: + + /** + * @hide + */ + public static final int PENALTY_DIALOG = 0x20; + + /** + * Death on any detected violation. + * + * @hide + */ + public static final int PENALTY_DEATH = 0x40; + + /** + * Death just for detected network usage. + * + * @hide + */ + public static final int PENALTY_DEATH_ON_NETWORK = 0x200; + + /** + * Flash the screen during violations. + * + * @hide + */ + public static final int PENALTY_FLASH = 0x800; + + /** + * @hide + */ + public static final int PENALTY_DROPBOX = 0x80; + + /** + * Non-public penalty mode which overrides all the other penalty + * bits and signals that we're in a Binder call and we should + * ignore the other penalty bits and instead serialize back all + * our offending stack traces to the caller to ultimately handle + * in the originating process. + * + * This must be kept in sync with the constant in libs/binder/Parcel.cpp + * + * @hide + */ + public static final int PENALTY_GATHER = 0x100; + + /** + * Mask of all the penalty bits valid for thread policies. + */ + private static final int THREAD_PENALTY_MASK = + PENALTY_LOG | PENALTY_DIALOG | PENALTY_DEATH | PENALTY_DROPBOX | PENALTY_GATHER | + PENALTY_DEATH_ON_NETWORK | PENALTY_FLASH; + + + /** + * Mask of all the penalty bits valid for VM policies. + */ + private static final int VM_PENALTY_MASK = + PENALTY_LOG | PENALTY_DEATH | PENALTY_DROPBOX; + + + // TODO: wrap in some ImmutableHashMap thing. + // Note: must be before static initialization of sVmPolicy. + private static final HashMap EMPTY_CLASS_LIMIT_MAP = new HashMap(); + + /** + * The current VmPolicy in effect. + * + * TODO: these are redundant (mask is in VmPolicy). Should remove sVmPolicyMask. + */ + private static volatile int sVmPolicyMask = 0; + private static volatile VmPolicy sVmPolicy = VmPolicy.LAX; + + /** + * The number of threads trying to do an async dropbox write. + * Just to limit ourselves out of paranoia. + */ + private static final AtomicInteger sDropboxCallsInFlight = new AtomicInteger(0); + + private StrictMode() {} + + /** + * {@link StrictMode} policy applied to a certain thread. + * + *

    The policy is enabled by {@link #setThreadPolicy}. The current policy + * can be retrieved with {@link #getThreadPolicy}. + * + *

    Note that multiple penalties may be provided and they're run + * in order from least to most severe (logging before process + * death, for example). There's currently no mechanism to choose + * different penalties for different detected actions. + */ + public static final class ThreadPolicy { + /** + * The default, lax policy which doesn't catch anything. + */ + public static final ThreadPolicy LAX = new ThreadPolicy(0); + + final int mask; + + private ThreadPolicy(int mask) { + this.mask = mask; + } + + @Override + public String toString() { + return "[StrictMode.ThreadPolicy; mask=" + mask + "]"; + } + + /** + * Creates {@link ThreadPolicy} instances. Methods whose names start + * with {@code detect} specify what problems we should look + * for. Methods whose names start with {@code penalty} specify what + * we should do when we detect a problem. + * + *

    You can call as many {@code detect} and {@code penalty} + * methods as you like. Currently order is insignificant: all + * penalties apply to all detected problems. + * + *

    For example, detect everything and log anything that's found: + *

    +         * StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder()
    +         *     .detectAll()
    +         *     .penaltyLog()
    +         *     .build();
    +         * StrictMode.setThreadPolicy(policy);
    +         * 
    + */ + public static final class Builder { + private int mMask = 0; + + /** + * Create a Builder that detects nothing and has no + * violations. (but note that {@link #build} will default + * to enabling {@link #penaltyLog} if no other penalties + * are specified) + */ + public Builder() { + mMask = 0; + } + + /** + * Initialize a Builder from an existing ThreadPolicy. + */ + public Builder(ThreadPolicy policy) { + mMask = policy.mask; + } + + /** + * Detect everything that's potentially suspect. + * + *

    As of the Gingerbread release this includes network and + * disk operations but will likely expand in future releases. + */ + public Builder detectAll() { + return enable(ALL_THREAD_DETECT_BITS); + } + + /** + * Disable the detection of everything. + */ + public Builder permitAll() { + return disable(ALL_THREAD_DETECT_BITS); + } + + /** + * Enable detection of network operations. + */ + public Builder detectNetwork() { + return enable(DETECT_NETWORK); + } + + /** + * Disable detection of network operations. + */ + public Builder permitNetwork() { + return disable(DETECT_NETWORK); + } + + /** + * Enable detection of disk reads. + */ + public Builder detectDiskReads() { + return enable(DETECT_DISK_READ); + } + + /** + * Disable detection of disk reads. + */ + public Builder permitDiskReads() { + return disable(DETECT_DISK_READ); + } + + /** + * Enable detection of slow calls. + */ + public Builder detectCustomSlowCalls() { + return enable(DETECT_CUSTOM); + } + + /** + * Disable detection of slow calls. + */ + public Builder permitCustomSlowCalls() { + return disable(DETECT_CUSTOM); + } + + /** + * Enable detection of disk writes. + */ + public Builder detectDiskWrites() { + return enable(DETECT_DISK_WRITE); + } + + /** + * Disable detection of disk writes. + */ + public Builder permitDiskWrites() { + return disable(DETECT_DISK_WRITE); + } + + /** + * Show an annoying dialog to the developer on detected + * violations, rate-limited to be only a little annoying. + */ + public Builder penaltyDialog() { + return enable(PENALTY_DIALOG); + } + + /** + * Crash the whole process on violation. This penalty runs at + * the end of all enabled penalties so you'll still get + * see logging or other violations before the process dies. + * + *

    Unlike {@link #penaltyDeathOnNetwork}, this applies + * to disk reads, disk writes, and network usage if their + * corresponding detect flags are set. + */ + public Builder penaltyDeath() { + return enable(PENALTY_DEATH); + } + + /** + * Crash the whole process on any network usage. Unlike + * {@link #penaltyDeath}, this penalty runs + * before anything else. You must still have + * called {@link #detectNetwork} to enable this. + * + *

    In the Honeycomb or later SDKs, this is on by default. + */ + public Builder penaltyDeathOnNetwork() { + return enable(PENALTY_DEATH_ON_NETWORK); + } + + /** + * Flash the screen during a violation. + */ + public Builder penaltyFlashScreen() { + return enable(PENALTY_FLASH); + } + + /** + * Log detected violations to the system log. + */ + public Builder penaltyLog() { + return enable(PENALTY_LOG); + } + + /** + * Enable detected violations log a stacktrace and timing data + * to the {@link android.os.DropBoxManager DropBox} on policy + * violation. Intended mostly for platform integrators doing + * beta user field data collection. + */ + public Builder penaltyDropBox() { + return enable(PENALTY_DROPBOX); + } + + private Builder enable(int bit) { + mMask |= bit; + return this; + } + + private Builder disable(int bit) { + mMask &= ~bit; + return this; + } + + /** + * Construct the ThreadPolicy instance. + * + *

    Note: if no penalties are enabled before calling + * build, {@link #penaltyLog} is implicitly + * set. + */ + public ThreadPolicy build() { + // If there are detection bits set but no violation bits + // set, enable simple logging. + if (mMask != 0 && + (mMask & (PENALTY_DEATH | PENALTY_LOG | + PENALTY_DROPBOX | PENALTY_DIALOG)) == 0) { + penaltyLog(); + } + return new ThreadPolicy(mMask); + } + } + } + + /** + * {@link StrictMode} policy applied to all threads in the virtual machine's process. + * + *

    The policy is enabled by {@link #setVmPolicy}. + */ + public static final class VmPolicy { + /** + * The default, lax policy which doesn't catch anything. + */ + public static final VmPolicy LAX = new VmPolicy(0, EMPTY_CLASS_LIMIT_MAP); + + final int mask; + + // Map from class to max number of allowed instances in memory. + final HashMap classInstanceLimit; + + private VmPolicy(int mask, HashMap classInstanceLimit) { + if (classInstanceLimit == null) { + throw new NullPointerException("classInstanceLimit == null"); + } + this.mask = mask; + this.classInstanceLimit = classInstanceLimit; + } + + @Override + public String toString() { + return "[StrictMode.VmPolicy; mask=" + mask + "]"; + } + + /** + * Creates {@link VmPolicy} instances. Methods whose names start + * with {@code detect} specify what problems we should look + * for. Methods whose names start with {@code penalty} specify what + * we should do when we detect a problem. + * + *

    You can call as many {@code detect} and {@code penalty} + * methods as you like. Currently order is insignificant: all + * penalties apply to all detected problems. + * + *

    For example, detect everything and log anything that's found: + *

    +         * StrictMode.VmPolicy policy = new StrictMode.VmPolicy.Builder()
    +         *     .detectAll()
    +         *     .penaltyLog()
    +         *     .build();
    +         * StrictMode.setVmPolicy(policy);
    +         * 
    + */ + public static final class Builder { + private int mMask; + + private HashMap mClassInstanceLimit; // null until needed + private boolean mClassInstanceLimitNeedCow = false; // need copy-on-write + + public Builder() { + mMask = 0; + } + + /** + * Build upon an existing VmPolicy. + */ + public Builder(VmPolicy base) { + mMask = base.mask; + mClassInstanceLimitNeedCow = true; + mClassInstanceLimit = base.classInstanceLimit; + } + + /** + * Set an upper bound on how many instances of a class can be in memory + * at once. Helps to prevent object leaks. + */ + public Builder setClassInstanceLimit(Class klass, int instanceLimit) { + if (klass == null) { + throw new NullPointerException("klass == null"); + } + if (mClassInstanceLimitNeedCow) { + if (mClassInstanceLimit.containsKey(klass) && + mClassInstanceLimit.get(klass) == instanceLimit) { + // no-op; don't break COW + return this; + } + mClassInstanceLimitNeedCow = false; + mClassInstanceLimit = (HashMap) mClassInstanceLimit.clone(); + } else if (mClassInstanceLimit == null) { + mClassInstanceLimit = new HashMap(); + } + mMask |= DETECT_VM_INSTANCE_LEAKS; + mClassInstanceLimit.put(klass, instanceLimit); + return this; + } + + /** + * Detect leaks of {@link android.app.Activity} subclasses. + */ + public Builder detectActivityLeaks() { + return enable(DETECT_VM_ACTIVITY_LEAKS); + } + + /** + * Detect everything that's potentially suspect. + * + *

    In the Honeycomb release this includes leaks of + * SQLite cursors, Activities, and other closable objects + * but will likely expand in future releases. + */ + public Builder detectAll() { + return enable(DETECT_VM_ACTIVITY_LEAKS | DETECT_VM_CURSOR_LEAKS + | DETECT_VM_CLOSABLE_LEAKS | DETECT_VM_REGISTRATION_LEAKS + | DETECT_VM_FILE_URI_EXPOSURE); + } + + /** + * Detect when an + * {@link android.database.sqlite.SQLiteCursor} or other + * SQLite object is finalized without having been closed. + * + *

    You always want to explicitly close your SQLite + * cursors to avoid unnecessary database contention and + * temporary memory leaks. + */ + public Builder detectLeakedSqlLiteObjects() { + return enable(DETECT_VM_CURSOR_LEAKS); + } + + /** + * Detect when an {@link java.io.Closeable} or other + * object with a explict termination method is finalized + * without having been closed. + * + *

    You always want to explicitly close such objects to + * avoid unnecessary resources leaks. + */ + public Builder detectLeakedClosableObjects() { + return enable(DETECT_VM_CLOSABLE_LEAKS); + } + + /** + * Detect when a {@link BroadcastReceiver} or + * {@link ServiceConnection} is leaked during {@link Context} + * teardown. + */ + public Builder detectLeakedRegistrationObjects() { + return enable(DETECT_VM_REGISTRATION_LEAKS); + } + + /** + * Detect when a {@code file://} {@link android.net.Uri} is exposed beyond this + * app. The receiving app may not have access to the sent path. + * Instead, when sharing files between apps, {@code content://} + * should be used with permission grants. + */ + public Builder detectFileUriExposure() { + return enable(DETECT_VM_FILE_URI_EXPOSURE); + } + + /** + * Crashes the whole process on violation. This penalty runs at + * the end of all enabled penalties so yo you'll still get + * your logging or other violations before the process dies. + */ + public Builder penaltyDeath() { + return enable(PENALTY_DEATH); + } + + /** + * Log detected violations to the system log. + */ + public Builder penaltyLog() { + return enable(PENALTY_LOG); + } + + /** + * Enable detected violations log a stacktrace and timing data + * to the {@link android.os.DropBoxManager DropBox} on policy + * violation. Intended mostly for platform integrators doing + * beta user field data collection. + */ + public Builder penaltyDropBox() { + return enable(PENALTY_DROPBOX); + } + + private Builder enable(int bit) { + mMask |= bit; + return this; + } + + /** + * Construct the VmPolicy instance. + * + *

    Note: if no penalties are enabled before calling + * build, {@link #penaltyLog} is implicitly + * set. + */ + public VmPolicy build() { + // If there are detection bits set but no violation bits + // set, enable simple logging. + if (mMask != 0 && + (mMask & (PENALTY_DEATH | PENALTY_LOG | + PENALTY_DROPBOX | PENALTY_DIALOG)) == 0) { + penaltyLog(); + } + return new VmPolicy(mMask, + mClassInstanceLimit != null ? mClassInstanceLimit : EMPTY_CLASS_LIMIT_MAP); + } + } + } + + /** + * Log of strict mode violation stack traces that have occurred + * during a Binder call, to be serialized back later to the caller + * via Parcel.writeNoException() (amusingly) where the caller can + * choose how to react. + */ + private static final ThreadLocal> gatheredViolations = + new ThreadLocal>() { + @Override protected ArrayList initialValue() { + // Starts null to avoid unnecessary allocations when + // checking whether there are any violations or not in + // hasGatheredViolations() below. + return null; + } + }; + + /** + * Sets the policy for what actions on the current thread should + * be detected, as well as the penalty if such actions occur. + * + *

    Internally this sets a thread-local variable which is + * propagated across cross-process IPC calls, meaning you can + * catch violations when a system service or another process + * accesses the disk or network on your behalf. + * + * @param policy the policy to put into place + */ + public static void setThreadPolicy(final ThreadPolicy policy) { + setThreadPolicyMask(policy.mask); + } + + private static void setThreadPolicyMask(final int policyMask) { + // In addition to the Java-level thread-local in Dalvik's + // BlockGuard, we also need to keep a native thread-local in + // Binder in order to propagate the value across Binder calls, + // even across native-only processes. The two are kept in + // sync via the callback to onStrictModePolicyChange, below. + setBlockGuardPolicy(policyMask); + + // And set the Android native version... + Binder.setThreadStrictModePolicy(policyMask); + } + + // Sets the policy in Dalvik/libcore (BlockGuard) + private static void setBlockGuardPolicy(final int policyMask) { + if (policyMask == 0) { + BlockGuard.setThreadPolicy(BlockGuard.LAX_POLICY); + return; + } + final BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); + final AndroidBlockGuardPolicy androidPolicy; + if (policy instanceof AndroidBlockGuardPolicy) { + androidPolicy = (AndroidBlockGuardPolicy) policy; + } else { + androidPolicy = threadAndroidPolicy.get(); + BlockGuard.setThreadPolicy(androidPolicy); + } + androidPolicy.setPolicyMask(policyMask); + } + + // Sets up CloseGuard in Dalvik/libcore + private static void setCloseGuardEnabled(boolean enabled) { + if (!(CloseGuard.getReporter() instanceof AndroidCloseGuardReporter)) { + CloseGuard.setReporter(new AndroidCloseGuardReporter()); + } + CloseGuard.setEnabled(enabled); + } + + /** + * @hide + */ + public static class StrictModeViolation extends BlockGuard.BlockGuardPolicyException { + public StrictModeViolation(int policyState, int policyViolated, String message) { + super(policyState, policyViolated, message); + } + } + + /** + * @hide + */ + public static class StrictModeNetworkViolation extends StrictModeViolation { + public StrictModeNetworkViolation(int policyMask) { + super(policyMask, DETECT_NETWORK, null); + } + } + + /** + * @hide + */ + private static class StrictModeDiskReadViolation extends StrictModeViolation { + public StrictModeDiskReadViolation(int policyMask) { + super(policyMask, DETECT_DISK_READ, null); + } + } + + /** + * @hide + */ + private static class StrictModeDiskWriteViolation extends StrictModeViolation { + public StrictModeDiskWriteViolation(int policyMask) { + super(policyMask, DETECT_DISK_WRITE, null); + } + } + + /** + * @hide + */ + private static class StrictModeCustomViolation extends StrictModeViolation { + public StrictModeCustomViolation(int policyMask, String name) { + super(policyMask, DETECT_CUSTOM, name); + } + } + + /** + * Returns the bitmask of the current thread's policy. + * + * @return the bitmask of all the DETECT_* and PENALTY_* bits currently enabled + * + * @hide + */ + public static int getThreadPolicyMask() { + return BlockGuard.getThreadPolicy().getPolicyMask(); + } + + /** + * Returns the current thread's policy. + */ + public static ThreadPolicy getThreadPolicy() { + // TODO: this was a last minute Gingerbread API change (to + // introduce VmPolicy cleanly) but this isn't particularly + // optimal for users who might call this method often. This + // should be in a thread-local and not allocate on each call. + return new ThreadPolicy(getThreadPolicyMask()); + } + + /** + * A convenience wrapper that takes the current + * {@link ThreadPolicy} from {@link #getThreadPolicy}, modifies it + * to permit both disk reads & writes, and sets the new policy + * with {@link #setThreadPolicy}, returning the old policy so you + * can restore it at the end of a block. + * + * @return the old policy, to be passed to {@link #setThreadPolicy} to + * restore the policy at the end of a block + */ + public static ThreadPolicy allowThreadDiskWrites() { + int oldPolicyMask = getThreadPolicyMask(); + int newPolicyMask = oldPolicyMask & ~(DETECT_DISK_WRITE | DETECT_DISK_READ); + if (newPolicyMask != oldPolicyMask) { + setThreadPolicyMask(newPolicyMask); + } + return new ThreadPolicy(oldPolicyMask); + } + + /** + * A convenience wrapper that takes the current + * {@link ThreadPolicy} from {@link #getThreadPolicy}, modifies it + * to permit disk reads, and sets the new policy + * with {@link #setThreadPolicy}, returning the old policy so you + * can restore it at the end of a block. + * + * @return the old policy, to be passed to setThreadPolicy to + * restore the policy. + */ + public static ThreadPolicy allowThreadDiskReads() { + int oldPolicyMask = getThreadPolicyMask(); + int newPolicyMask = oldPolicyMask & ~(DETECT_DISK_READ); + if (newPolicyMask != oldPolicyMask) { + setThreadPolicyMask(newPolicyMask); + } + return new ThreadPolicy(oldPolicyMask); + } + + // We don't want to flash the screen red in the system server + // process, nor do we want to modify all the call sites of + // conditionallyEnableDebugLogging() in the system server, + // so instead we use this to determine if we are the system server. + private static boolean amTheSystemServerProcess() { + // Fast path. Most apps don't have the system server's UID. + if (Process.myUid() != Process.SYSTEM_UID) { + return false; + } + + // The settings app, though, has the system server's UID so + // look up our stack to see if we came from the system server. + Throwable stack = new Throwable(); + stack.fillInStackTrace(); + for (StackTraceElement ste : stack.getStackTrace()) { + String clsName = ste.getClassName(); + if (clsName != null && clsName.startsWith("com.android.server.")) { + return true; + } + } + return false; + } + + /** + * Enable DropBox logging for debug phone builds. + * + * @hide + */ + public static boolean conditionallyEnableDebugLogging() { + boolean doFlashes = SystemProperties.getBoolean(VISUAL_PROPERTY, false) + && !amTheSystemServerProcess(); + final boolean suppress = SystemProperties.getBoolean(DISABLE_PROPERTY, false); + + // For debug builds, log event loop stalls to dropbox for analysis. + // Similar logic also appears in ActivityThread.java for system apps. + if (!doFlashes && (IS_USER_BUILD || suppress)) { + setCloseGuardEnabled(false); + return false; + } + + // Eng builds have flashes on all the time. The suppression property + // overrides this, so we force the behavior only after the short-circuit + // check above. + if (IS_ENG_BUILD) { + doFlashes = true; + } + + // Thread policy controls BlockGuard. + int threadPolicyMask = StrictMode.DETECT_DISK_WRITE | + StrictMode.DETECT_DISK_READ | + StrictMode.DETECT_NETWORK; + + if (!IS_USER_BUILD) { + threadPolicyMask |= StrictMode.PENALTY_DROPBOX; + } + if (doFlashes) { + threadPolicyMask |= StrictMode.PENALTY_FLASH; + } + + StrictMode.setThreadPolicyMask(threadPolicyMask); + + // VM Policy controls CloseGuard, detection of Activity leaks, + // and instance counting. + if (IS_USER_BUILD) { + setCloseGuardEnabled(false); + } else { + VmPolicy.Builder policyBuilder = new VmPolicy.Builder().detectAll().penaltyDropBox(); + if (IS_ENG_BUILD) { + policyBuilder.penaltyLog(); + } + setVmPolicy(policyBuilder.build()); + setCloseGuardEnabled(vmClosableObjectLeaksEnabled()); + } + return true; + } + + /** + * Used by the framework to make network usage on the main + * thread a fatal error. + * + * @hide + */ + public static void enableDeathOnNetwork() { + int oldPolicy = getThreadPolicyMask(); + int newPolicy = oldPolicy | DETECT_NETWORK | PENALTY_DEATH_ON_NETWORK; + setThreadPolicyMask(newPolicy); + } + + /** + * Parses the BlockGuard policy mask out from the Exception's + * getMessage() String value. Kinda gross, but least + * invasive. :/ + * + * Input is of the following forms: + * "policy=137 violation=64" + * "policy=137 violation=64 msg=Arbitrary text" + * + * Returns 0 on failure, which is a valid policy, but not a + * valid policy during a violation (else there must've been + * some policy in effect to violate). + */ + private static int parsePolicyFromMessage(String message) { + if (message == null || !message.startsWith("policy=")) { + return 0; + } + int spaceIndex = message.indexOf(' '); + if (spaceIndex == -1) { + return 0; + } + String policyString = message.substring(7, spaceIndex); + try { + return Integer.valueOf(policyString).intValue(); + } catch (NumberFormatException e) { + return 0; + } + } + + /** + * Like parsePolicyFromMessage(), but returns the violation. + */ + private static int parseViolationFromMessage(String message) { + if (message == null) { + return 0; + } + int violationIndex = message.indexOf("violation="); + if (violationIndex == -1) { + return 0; + } + int numberStartIndex = violationIndex + "violation=".length(); + int numberEndIndex = message.indexOf(' ', numberStartIndex); + if (numberEndIndex == -1) { + numberEndIndex = message.length(); + } + String violationString = message.substring(numberStartIndex, numberEndIndex); + try { + return Integer.valueOf(violationString).intValue(); + } catch (NumberFormatException e) { + return 0; + } + } + + private static final ThreadLocal> violationsBeingTimed = + new ThreadLocal>() { + @Override protected ArrayList initialValue() { + return new ArrayList(); + } + }; + + // Note: only access this once verifying the thread has a Looper. + private static final ThreadLocal threadHandler = new ThreadLocal() { + @Override protected Handler initialValue() { + return new Handler(); + } + }; + + private static final ThreadLocal + threadAndroidPolicy = new ThreadLocal() { + @Override + protected AndroidBlockGuardPolicy initialValue() { + return new AndroidBlockGuardPolicy(0); + } + }; + + private static boolean tooManyViolationsThisLoop() { + return violationsBeingTimed.get().size() >= MAX_OFFENSES_PER_LOOP; + } + + private static class AndroidBlockGuardPolicy implements BlockGuard.Policy { + private int mPolicyMask; + + // Map from violation stacktrace hashcode -> uptimeMillis of + // last violation. No locking needed, as this is only + // accessed by the same thread. + private ArrayMap mLastViolationTime; + + public AndroidBlockGuardPolicy(final int policyMask) { + mPolicyMask = policyMask; + } + + @Override + public String toString() { + return "AndroidBlockGuardPolicy; mPolicyMask=" + mPolicyMask; + } + + // Part of BlockGuard.Policy interface: + public int getPolicyMask() { + return mPolicyMask; + } + + // Part of BlockGuard.Policy interface: + public void onWriteToDisk() { + if ((mPolicyMask & DETECT_DISK_WRITE) == 0) { + return; + } + if (tooManyViolationsThisLoop()) { + return; + } + BlockGuard.BlockGuardPolicyException e = new StrictModeDiskWriteViolation(mPolicyMask); + e.fillInStackTrace(); + startHandlingViolationException(e); + } + + // Not part of BlockGuard.Policy; just part of StrictMode: + void onCustomSlowCall(String name) { + if ((mPolicyMask & DETECT_CUSTOM) == 0) { + return; + } + if (tooManyViolationsThisLoop()) { + return; + } + BlockGuard.BlockGuardPolicyException e = new StrictModeCustomViolation(mPolicyMask, name); + e.fillInStackTrace(); + startHandlingViolationException(e); + } + + // Part of BlockGuard.Policy interface: + public void onReadFromDisk() { + if ((mPolicyMask & DETECT_DISK_READ) == 0) { + return; + } + if (tooManyViolationsThisLoop()) { + return; + } + BlockGuard.BlockGuardPolicyException e = new StrictModeDiskReadViolation(mPolicyMask); + e.fillInStackTrace(); + startHandlingViolationException(e); + } + + // Part of BlockGuard.Policy interface: + public void onNetwork() { + if ((mPolicyMask & DETECT_NETWORK) == 0) { + return; + } + if ((mPolicyMask & PENALTY_DEATH_ON_NETWORK) != 0) { + throw new NetworkOnMainThreadException(); + } + if (tooManyViolationsThisLoop()) { + return; + } + BlockGuard.BlockGuardPolicyException e = new StrictModeNetworkViolation(mPolicyMask); + e.fillInStackTrace(); + startHandlingViolationException(e); + } + + public void setPolicyMask(int policyMask) { + mPolicyMask = policyMask; + } + + // Start handling a violation that just started and hasn't + // actually run yet (e.g. no disk write or network operation + // has yet occurred). This sees if we're in an event loop + // thread and, if so, uses it to roughly measure how long the + // violation took. + void startHandlingViolationException(BlockGuard.BlockGuardPolicyException e) { + final ViolationInfo info = new ViolationInfo(e, e.getPolicy()); + info.violationUptimeMillis = SystemClock.uptimeMillis(); + handleViolationWithTimingAttempt(info); + } + + // Attempts to fill in the provided ViolationInfo's + // durationMillis field if this thread has a Looper we can use + // to measure with. We measure from the time of violation + // until the time the looper is idle again (right before + // the next epoll_wait) + void handleViolationWithTimingAttempt(final ViolationInfo info) { + Looper looper = Looper.myLooper(); + + // Without a Looper, we're unable to time how long the + // violation takes place. This case should be rare, as + // most users will care about timing violations that + // happen on their main UI thread. Note that this case is + // also hit when a violation takes place in a Binder + // thread, in "gather" mode. In this case, the duration + // of the violation is computed by the ultimate caller and + // its Looper, if any. + // + // Also, as a special short-cut case when the only penalty + // bit is death, we die immediately, rather than timing + // the violation's duration. This makes it convenient to + // use in unit tests too, rather than waiting on a Looper. + // + // TODO: if in gather mode, ignore Looper.myLooper() and always + // go into this immediate mode? + if (looper == null || + (info.policy & THREAD_PENALTY_MASK) == PENALTY_DEATH) { + info.durationMillis = -1; // unknown (redundant, already set) + handleViolation(info); + return; + } + + final ArrayList records = violationsBeingTimed.get(); + if (records.size() >= MAX_OFFENSES_PER_LOOP) { + // Not worth measuring. Too many offenses in one loop. + return; + } + records.add(info); + if (records.size() > 1) { + // There's already been a violation this loop, so we've already + // registered an idle handler to process the list of violations + // at the end of this Looper's loop. + return; + } + + final IWindowManager windowManager = (info.policy & PENALTY_FLASH) != 0 ? + sWindowManager.get() : null; + if (windowManager != null) { + try { + windowManager.showStrictModeViolation(true); + } catch (RemoteException unused) { + } + } + + // We post a runnable to a Handler (== delay 0 ms) for + // measuring the end time of a violation instead of using + // an IdleHandler (as was previously used) because an + // IdleHandler may not run for quite a long period of time + // if an ongoing animation is happening and continually + // posting ASAP (0 ms) animation steps. Animations are + // throttled back to 60fps via SurfaceFlinger/View + // invalidates, _not_ by posting frame updates every 16 + // milliseconds. + threadHandler.get().postAtFrontOfQueue(new Runnable() { + public void run() { + long loopFinishTime = SystemClock.uptimeMillis(); + + // Note: we do this early, before handling the + // violation below, as handling the violation + // may include PENALTY_DEATH and we don't want + // to keep the red border on. + if (windowManager != null) { + try { + windowManager.showStrictModeViolation(false); + } catch (RemoteException unused) { + } + } + + for (int n = 0; n < records.size(); ++n) { + ViolationInfo v = records.get(n); + v.violationNumThisLoop = n + 1; + v.durationMillis = + (int) (loopFinishTime - v.violationUptimeMillis); + handleViolation(v); + } + records.clear(); + } + }); + } + + // Note: It's possible (even quite likely) that the + // thread-local policy mask has changed from the time the + // violation fired and now (after the violating code ran) due + // to people who push/pop temporary policy in regions of code, + // hence the policy being passed around. + void handleViolation(final ViolationInfo info) { + if (info == null || info.crashInfo == null || info.crashInfo.stackTrace == null) { + Log.wtf(TAG, "unexpected null stacktrace"); + return; + } + + if (LOG_V) Log.d(TAG, "handleViolation; policy=" + info.policy); + + if ((info.policy & PENALTY_GATHER) != 0) { + ArrayList violations = gatheredViolations.get(); + if (violations == null) { + violations = new ArrayList(1); + gatheredViolations.set(violations); + } else if (violations.size() >= 5) { + // Too many. In a loop or something? Don't gather them all. + return; + } + for (ViolationInfo previous : violations) { + if (info.crashInfo.stackTrace.equals(previous.crashInfo.stackTrace)) { + // Duplicate. Don't log. + return; + } + } + violations.add(info); + return; + } + + // Not perfect, but fast and good enough for dup suppression. + Integer crashFingerprint = info.hashCode(); + long lastViolationTime = 0; + if (mLastViolationTime != null) { + Long vtime = mLastViolationTime.get(crashFingerprint); + if (vtime != null) { + lastViolationTime = vtime; + } + } else { + mLastViolationTime = new ArrayMap(1); + } + long now = SystemClock.uptimeMillis(); + mLastViolationTime.put(crashFingerprint, now); + long timeSinceLastViolationMillis = lastViolationTime == 0 ? + Long.MAX_VALUE : (now - lastViolationTime); + + if ((info.policy & PENALTY_LOG) != 0 && + timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { + if (info.durationMillis != -1) { + Log.d(TAG, "StrictMode policy violation; ~duration=" + + info.durationMillis + " ms: " + info.crashInfo.stackTrace); + } else { + Log.d(TAG, "StrictMode policy violation: " + info.crashInfo.stackTrace); + } + } + + // The violationMaskSubset, passed to ActivityManager, is a + // subset of the original StrictMode policy bitmask, with + // only the bit violated and penalty bits to be executed + // by the ActivityManagerService remaining set. + int violationMaskSubset = 0; + + if ((info.policy & PENALTY_DIALOG) != 0 && + timeSinceLastViolationMillis > MIN_DIALOG_INTERVAL_MS) { + violationMaskSubset |= PENALTY_DIALOG; + } + + if ((info.policy & PENALTY_DROPBOX) != 0 && lastViolationTime == 0) { + violationMaskSubset |= PENALTY_DROPBOX; + } + + if (violationMaskSubset != 0) { + int violationBit = parseViolationFromMessage(info.crashInfo.exceptionMessage); + violationMaskSubset |= violationBit; + final int savedPolicyMask = getThreadPolicyMask(); + + final boolean justDropBox = (info.policy & THREAD_PENALTY_MASK) == PENALTY_DROPBOX; + if (justDropBox) { + // If all we're going to ask the activity manager + // to do is dropbox it (the common case during + // platform development), we can avoid doing this + // call synchronously which Binder data suggests + // isn't always super fast, despite the implementation + // in the ActivityManager trying to be mostly async. + dropboxViolationAsync(violationMaskSubset, info); + return; + } + + // Normal synchronous call to the ActivityManager. + try { + // First, remove any policy before we call into the Activity Manager, + // otherwise we'll infinite recurse as we try to log policy violations + // to disk, thus violating policy, thus requiring logging, etc... + // We restore the current policy below, in the finally block. + setThreadPolicyMask(0); + + ActivityManagerNative.getDefault().handleApplicationStrictModeViolation( + RuntimeInit.getApplicationObject(), + violationMaskSubset, + info); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException trying to handle StrictMode violation", e); + } finally { + // Restore the policy. + setThreadPolicyMask(savedPolicyMask); + } + } + + if ((info.policy & PENALTY_DEATH) != 0) { + executeDeathPenalty(info); + } + } + } + + private static void executeDeathPenalty(ViolationInfo info) { + int violationBit = parseViolationFromMessage(info.crashInfo.exceptionMessage); + throw new StrictModeViolation(info.policy, violationBit, null); + } + + /** + * In the common case, as set by conditionallyEnableDebugLogging, + * we're just dropboxing any violations but not showing a dialog, + * not loggging, and not killing the process. In these cases we + * don't need to do a synchronous call to the ActivityManager. + * This is used by both per-thread and vm-wide violations when + * applicable. + */ + private static void dropboxViolationAsync( + final int violationMaskSubset, final ViolationInfo info) { + int outstanding = sDropboxCallsInFlight.incrementAndGet(); + if (outstanding > 20) { + // What's going on? Let's not make make the situation + // worse and just not log. + sDropboxCallsInFlight.decrementAndGet(); + return; + } + + if (LOG_V) Log.d(TAG, "Dropboxing async; in-flight=" + outstanding); + + new Thread("callActivityManagerForStrictModeDropbox") { + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + try { + IActivityManager am = ActivityManagerNative.getDefault(); + if (am == null) { + Log.d(TAG, "No activity manager; failed to Dropbox violation."); + } else { + am.handleApplicationStrictModeViolation( + RuntimeInit.getApplicationObject(), + violationMaskSubset, + info); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException handling StrictMode violation", e); + } + int outstanding = sDropboxCallsInFlight.decrementAndGet(); + if (LOG_V) Log.d(TAG, "Dropbox complete; in-flight=" + outstanding); + } + }.start(); + } + + private static class AndroidCloseGuardReporter implements CloseGuard.Reporter { + public void report (String message, Throwable allocationSite) { + onVmPolicyViolation(message, allocationSite); + } + } + + /** + * Called from Parcel.writeNoException() + */ + /* package */ static boolean hasGatheredViolations() { + return gatheredViolations.get() != null; + } + + /** + * Called from Parcel.writeException(), so we drop this memory and + * don't incorrectly attribute it to the wrong caller on the next + * Binder call on this thread. + */ + /* package */ static void clearGatheredViolations() { + gatheredViolations.set(null); + } + + /** + * @hide + */ + public static void conditionallyCheckInstanceCounts() { + VmPolicy policy = getVmPolicy(); + if (policy.classInstanceLimit.size() == 0) { + return; + } + + System.gc(); + System.runFinalization(); + System.gc(); + + // Note: classInstanceLimit is immutable, so this is lock-free + for (Map.Entry entry : policy.classInstanceLimit.entrySet()) { + Class klass = entry.getKey(); + int limit = entry.getValue(); + long instances = VMDebug.countInstancesOfClass(klass, false); + if (instances <= limit) { + continue; + } + Throwable tr = new InstanceCountViolation(klass, instances, limit); + onVmPolicyViolation(tr.getMessage(), tr); + } + } + + private static long sLastInstanceCountCheckMillis = 0; + private static boolean sIsIdlerRegistered = false; // guarded by StrictMode.class + private static final MessageQueue.IdleHandler sProcessIdleHandler = + new MessageQueue.IdleHandler() { + public boolean queueIdle() { + long now = SystemClock.uptimeMillis(); + if (now - sLastInstanceCountCheckMillis > 30 * 1000) { + sLastInstanceCountCheckMillis = now; + conditionallyCheckInstanceCounts(); + } + return true; + } + }; + + /** + * Sets the policy for what actions in the VM process (on any + * thread) should be detected, as well as the penalty if such + * actions occur. + * + * @param policy the policy to put into place + */ + public static void setVmPolicy(final VmPolicy policy) { + synchronized (StrictMode.class) { + sVmPolicy = policy; + sVmPolicyMask = policy.mask; + setCloseGuardEnabled(vmClosableObjectLeaksEnabled()); + + Looper looper = Looper.getMainLooper(); + if (looper != null) { + MessageQueue mq = looper.mQueue; + if (policy.classInstanceLimit.size() == 0 || + (sVmPolicyMask & VM_PENALTY_MASK) == 0) { + mq.removeIdleHandler(sProcessIdleHandler); + sIsIdlerRegistered = false; + } else if (!sIsIdlerRegistered) { + mq.addIdleHandler(sProcessIdleHandler); + sIsIdlerRegistered = true; + } + } + } + } + + /** + * Gets the current VM policy. + */ + public static VmPolicy getVmPolicy() { + synchronized (StrictMode.class) { + return sVmPolicy; + } + } + + /** + * Enable the recommended StrictMode defaults, with violations just being logged. + * + *

    This catches disk and network access on the main thread, as + * well as leaked SQLite cursors and unclosed resources. This is + * simply a wrapper around {@link #setVmPolicy} and {@link + * #setThreadPolicy}. + */ + public static void enableDefaults() { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + .build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog() + .build()); + } + + /** + * @hide + */ + public static boolean vmSqliteObjectLeaksEnabled() { + return (sVmPolicyMask & DETECT_VM_CURSOR_LEAKS) != 0; + } + + /** + * @hide + */ + public static boolean vmClosableObjectLeaksEnabled() { + return (sVmPolicyMask & DETECT_VM_CLOSABLE_LEAKS) != 0; + } + + /** + * @hide + */ + public static boolean vmRegistrationLeaksEnabled() { + return (sVmPolicyMask & DETECT_VM_REGISTRATION_LEAKS) != 0; + } + + /** + * @hide + */ + public static boolean vmFileUriExposureEnabled() { + return (sVmPolicyMask & DETECT_VM_FILE_URI_EXPOSURE) != 0; + } + + /** + * @hide + */ + public static void onSqliteObjectLeaked(String message, Throwable originStack) { + onVmPolicyViolation(message, originStack); + } + + /** + * @hide + */ + public static void onWebViewMethodCalledOnWrongThread(Throwable originStack) { + onVmPolicyViolation(null, originStack); + } + + /** + * @hide + */ + public static void onIntentReceiverLeaked(Throwable originStack) { + onVmPolicyViolation(null, originStack); + } + + /** + * @hide + */ + public static void onServiceConnectionLeaked(Throwable originStack) { + onVmPolicyViolation(null, originStack); + } + + /** + * @hide + */ + public static void onFileUriExposed(String location) { + final String message = "file:// Uri exposed through " + location; + onVmPolicyViolation(message, new Throwable(message)); + } + + // Map from VM violation fingerprint to uptime millis. + private static final HashMap sLastVmViolationTime = new HashMap(); + + /** + * @hide + */ + public static void onVmPolicyViolation(String message, Throwable originStack) { + final boolean penaltyDropbox = (sVmPolicyMask & PENALTY_DROPBOX) != 0; + final boolean penaltyDeath = (sVmPolicyMask & PENALTY_DEATH) != 0; + final boolean penaltyLog = (sVmPolicyMask & PENALTY_LOG) != 0; + final ViolationInfo info = new ViolationInfo(originStack, sVmPolicyMask); + + // Erase stuff not relevant for process-wide violations + info.numAnimationsRunning = 0; + info.tags = null; + info.broadcastIntentAction = null; + + final Integer fingerprint = info.hashCode(); + final long now = SystemClock.uptimeMillis(); + long lastViolationTime = 0; + long timeSinceLastViolationMillis = Long.MAX_VALUE; + synchronized (sLastVmViolationTime) { + if (sLastVmViolationTime.containsKey(fingerprint)) { + lastViolationTime = sLastVmViolationTime.get(fingerprint); + timeSinceLastViolationMillis = now - lastViolationTime; + } + if (timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { + sLastVmViolationTime.put(fingerprint, now); + } + } + + if (penaltyLog && timeSinceLastViolationMillis > MIN_LOG_INTERVAL_MS) { + Log.e(TAG, message, originStack); + } + + int violationMaskSubset = PENALTY_DROPBOX | (ALL_VM_DETECT_BITS & sVmPolicyMask); + + if (penaltyDropbox && !penaltyDeath) { + // Common case for userdebug/eng builds. If no death and + // just dropboxing, we can do the ActivityManager call + // asynchronously. + dropboxViolationAsync(violationMaskSubset, info); + return; + } + + if (penaltyDropbox && lastViolationTime == 0) { + // The violationMask, passed to ActivityManager, is a + // subset of the original StrictMode policy bitmask, with + // only the bit violated and penalty bits to be executed + // by the ActivityManagerService remaining set. + final int savedPolicyMask = getThreadPolicyMask(); + try { + // First, remove any policy before we call into the Activity Manager, + // otherwise we'll infinite recurse as we try to log policy violations + // to disk, thus violating policy, thus requiring logging, etc... + // We restore the current policy below, in the finally block. + setThreadPolicyMask(0); + + ActivityManagerNative.getDefault().handleApplicationStrictModeViolation( + RuntimeInit.getApplicationObject(), + violationMaskSubset, + info); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException trying to handle StrictMode violation", e); + } finally { + // Restore the policy. + setThreadPolicyMask(savedPolicyMask); + } + } + + if (penaltyDeath) { + System.err.println("StrictMode VmPolicy violation with POLICY_DEATH; shutting down."); + Process.killProcess(Process.myPid()); + System.exit(10); + } + } + + /** + * Called from Parcel.writeNoException() + */ + /* package */ static void writeGatheredViolationsToParcel(Parcel p) { + ArrayList violations = gatheredViolations.get(); + if (violations == null) { + p.writeInt(0); + } else { + p.writeInt(violations.size()); + for (int i = 0; i < violations.size(); ++i) { + int start = p.dataPosition(); + violations.get(i).writeToParcel(p, 0 /* unused flags? */); + int size = p.dataPosition()-start; + if (size > 10*1024) { + Slog.d(TAG, "Wrote violation #" + i + " of " + violations.size() + ": " + + (p.dataPosition()-start) + " bytes"); + } + } + if (LOG_V) Log.d(TAG, "wrote violations to response parcel; num=" + violations.size()); + violations.clear(); // somewhat redundant, as we're about to null the threadlocal + } + gatheredViolations.set(null); + } + + private static class LogStackTrace extends Exception {} + + /** + * Called from Parcel.readException() when the exception is EX_STRICT_MODE_VIOLATIONS, + * we here read back all the encoded violations. + */ + /* package */ static void readAndHandleBinderCallViolations(Parcel p) { + // Our own stack trace to append + StringWriter sw = new StringWriter(); + PrintWriter pw = new FastPrintWriter(sw, false, 256); + new LogStackTrace().printStackTrace(pw); + pw.flush(); + String ourStack = sw.toString(); + + int policyMask = getThreadPolicyMask(); + boolean currentlyGathering = (policyMask & PENALTY_GATHER) != 0; + + int numViolations = p.readInt(); + for (int i = 0; i < numViolations; ++i) { + if (LOG_V) Log.d(TAG, "strict mode violation stacks read from binder call. i=" + i); + ViolationInfo info = new ViolationInfo(p, !currentlyGathering); + if (info.crashInfo.stackTrace != null && info.crashInfo.stackTrace.length() > 10000) { + String front = info.crashInfo.stackTrace.substring(256); + // 10000 characters is way too large for this to be any sane kind of + // strict mode collection of stacks. We've had a problem where we leave + // strict mode violations associated with the thread, and it keeps tacking + // more and more stacks on to the violations. Looks like we're in this casse, + // so we'll report it and bail on all of the current strict mode violations + // we currently are maintaining for this thread. + // First, drain the remaining violations from the parcel. + while (i < numViolations) { + info = new ViolationInfo(p, !currentlyGathering); + i++; + } + // Next clear out all gathered violations. + clearGatheredViolations(); + // Now report the problem. + Slog.wtfStack(TAG, "Stack is too large: numViolations=" + numViolations + + " policy=#" + Integer.toHexString(policyMask) + + " front=" + front); + return; + } + info.crashInfo.stackTrace += "# via Binder call with stack:\n" + ourStack; + BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); + if (policy instanceof AndroidBlockGuardPolicy) { + ((AndroidBlockGuardPolicy) policy).handleViolationWithTimingAttempt(info); + } + } + } + + /** + * Called from android_util_Binder.cpp's + * android_os_Parcel_enforceInterface when an incoming Binder call + * requires changing the StrictMode policy mask. The role of this + * function is to ask Binder for its current (native) thread-local + * policy value and synchronize it to libcore's (Java) + * thread-local policy value. + */ + private static void onBinderStrictModePolicyChange(int newPolicy) { + setBlockGuardPolicy(newPolicy); + } + + /** + * A tracked, critical time span. (e.g. during an animation.) + * + * The object itself is a linked list node, to avoid any allocations + * during rapid span entries and exits. + * + * @hide + */ + public static class Span { + private String mName; + private long mCreateMillis; + private Span mNext; + private Span mPrev; // not used when in freeList, only active + private final ThreadSpanState mContainerState; + + Span(ThreadSpanState threadState) { + mContainerState = threadState; + } + + // Empty constructor for the NO_OP_SPAN + protected Span() { + mContainerState = null; + } + + /** + * To be called when the critical span is complete (i.e. the + * animation is done animating). This can be called on any + * thread (even a different one from where the animation was + * taking place), but that's only a defensive implementation + * measure. It really makes no sense for you to call this on + * thread other than that where you created it. + * + * @hide + */ + public void finish() { + ThreadSpanState state = mContainerState; + synchronized (state) { + if (mName == null) { + // Duplicate finish call. Ignore. + return; + } + + // Remove ourselves from the active list. + if (mPrev != null) { + mPrev.mNext = mNext; + } + if (mNext != null) { + mNext.mPrev = mPrev; + } + if (state.mActiveHead == this) { + state.mActiveHead = mNext; + } + + state.mActiveSize--; + + if (LOG_V) Log.d(TAG, "Span finished=" + mName + "; size=" + state.mActiveSize); + + this.mCreateMillis = -1; + this.mName = null; + this.mPrev = null; + this.mNext = null; + + // Add ourselves to the freeList, if it's not already + // too big. + if (state.mFreeListSize < 5) { + this.mNext = state.mFreeListHead; + state.mFreeListHead = this; + state.mFreeListSize++; + } + } + } + } + + // The no-op span that's used in user builds. + private static final Span NO_OP_SPAN = new Span() { + public void finish() { + // Do nothing. + } + }; + + /** + * Linked lists of active spans and a freelist. + * + * Locking notes: there's one of these structures per thread and + * all members of this structure (as well as the Span nodes under + * it) are guarded by the ThreadSpanState object instance. While + * in theory there'd be no locking required because it's all local + * per-thread, the finish() method above is defensive against + * people calling it on a different thread from where they created + * the Span, hence the locking. + */ + private static class ThreadSpanState { + public Span mActiveHead; // doubly-linked list. + public int mActiveSize; + public Span mFreeListHead; // singly-linked list. only changes at head. + public int mFreeListSize; + } + + private static final ThreadLocal sThisThreadSpanState = + new ThreadLocal() { + @Override protected ThreadSpanState initialValue() { + return new ThreadSpanState(); + } + }; + + private static Singleton sWindowManager = new Singleton() { + protected IWindowManager create() { + return IWindowManager.Stub.asInterface(ServiceManager.getService("window")); + } + }; + + /** + * Enter a named critical span (e.g. an animation) + * + *

    The name is an arbitary label (or tag) that will be applied + * to any strictmode violation that happens while this span is + * active. You must call finish() on the span when done. + * + *

    This will never return null, but on devices without debugging + * enabled, this may return a dummy object on which the finish() + * method is a no-op. + * + *

    TODO: add CloseGuard to this, verifying callers call finish. + * + * @hide + */ + public static Span enterCriticalSpan(String name) { + if (IS_USER_BUILD) { + return NO_OP_SPAN; + } + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name must be non-null and non-empty"); + } + ThreadSpanState state = sThisThreadSpanState.get(); + Span span = null; + synchronized (state) { + if (state.mFreeListHead != null) { + span = state.mFreeListHead; + state.mFreeListHead = span.mNext; + state.mFreeListSize--; + } else { + // Shouldn't have to do this often. + span = new Span(state); + } + span.mName = name; + span.mCreateMillis = SystemClock.uptimeMillis(); + span.mNext = state.mActiveHead; + span.mPrev = null; + state.mActiveHead = span; + state.mActiveSize++; + if (span.mNext != null) { + span.mNext.mPrev = span; + } + if (LOG_V) Log.d(TAG, "Span enter=" + name + "; size=" + state.mActiveSize); + } + return span; + } + + /** + * For code to note that it's slow. This is a no-op unless the + * current thread's {@link android.os.StrictMode.ThreadPolicy} has + * {@link android.os.StrictMode.ThreadPolicy.Builder#detectCustomSlowCalls} + * enabled. + * + * @param name a short string for the exception stack trace that's + * built if when this fires. + */ + public static void noteSlowCall(String name) { + BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); + if (!(policy instanceof AndroidBlockGuardPolicy)) { + // StrictMode not enabled. + return; + } + ((AndroidBlockGuardPolicy) policy).onCustomSlowCall(name); + } + + /** + * @hide + */ + public static void noteDiskRead() { + BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); + if (!(policy instanceof AndroidBlockGuardPolicy)) { + // StrictMode not enabled. + return; + } + ((AndroidBlockGuardPolicy) policy).onReadFromDisk(); + } + + /** + * @hide + */ + public static void noteDiskWrite() { + BlockGuard.Policy policy = BlockGuard.getThreadPolicy(); + if (!(policy instanceof AndroidBlockGuardPolicy)) { + // StrictMode not enabled. + return; + } + ((AndroidBlockGuardPolicy) policy).onWriteToDisk(); + } + + // Guarded by StrictMode.class + private static final HashMap sExpectedActivityInstanceCount = + new HashMap(); + + /** + * Returns an object that is used to track instances of activites. + * The activity should store a reference to the tracker object in one of its fields. + * @hide + */ + public static Object trackActivity(Object instance) { + return new InstanceTracker(instance); + } + + /** + * @hide + */ + public static void incrementExpectedActivityCount(Class klass) { + if (klass == null) { + return; + } + + synchronized (StrictMode.class) { + if ((sVmPolicy.mask & DETECT_VM_ACTIVITY_LEAKS) == 0) { + return; + } + + Integer expected = sExpectedActivityInstanceCount.get(klass); + Integer newExpected = expected == null ? 1 : expected + 1; + sExpectedActivityInstanceCount.put(klass, newExpected); + } + } + + /** + * @hide + */ + public static void decrementExpectedActivityCount(Class klass) { + if (klass == null) { + return; + } + + final int limit; + synchronized (StrictMode.class) { + if ((sVmPolicy.mask & DETECT_VM_ACTIVITY_LEAKS) == 0) { + return; + } + + Integer expected = sExpectedActivityInstanceCount.get(klass); + int newExpected = (expected == null || expected == 0) ? 0 : expected - 1; + if (newExpected == 0) { + sExpectedActivityInstanceCount.remove(klass); + } else { + sExpectedActivityInstanceCount.put(klass, newExpected); + } + + // Note: adding 1 here to give some breathing room during + // orientation changes. (shouldn't be necessary, though?) + limit = newExpected + 1; + } + + // Quick check. + int actual = InstanceTracker.getInstanceCount(klass); + if (actual <= limit) { + return; + } + + // Do a GC and explicit count to double-check. + // This is the work that we are trying to avoid by tracking the object instances + // explicity. Running an explicit GC can be expensive (80ms) and so can walking + // the heap to count instance (30ms). This extra work can make the system feel + // noticeably less responsive during orientation changes when activities are + // being restarted. Granted, it is only a problem when StrictMode is enabled + // but it is annoying. + + System.gc(); + System.runFinalization(); + System.gc(); + + long instances = VMDebug.countInstancesOfClass(klass, false); + if (instances > limit) { + Throwable tr = new InstanceCountViolation(klass, instances, limit); + onVmPolicyViolation(tr.getMessage(), tr); + } + } + + /** + * Parcelable that gets sent in Binder call headers back to callers + * to report violations that happened during a cross-process call. + * + * @hide + */ + public static class ViolationInfo { + /** + * Stack and other stuff info. + */ + public final ApplicationErrorReport.CrashInfo crashInfo; + + /** + * The strict mode policy mask at the time of violation. + */ + public final int policy; + + /** + * The wall time duration of the violation, when known. -1 when + * not known. + */ + public int durationMillis = -1; + + /** + * The number of animations currently running. + */ + public int numAnimationsRunning = 0; + + /** + * List of tags from active Span instances during this + * violation, or null for none. + */ + public String[] tags; + + /** + * Which violation number this was (1-based) since the last Looper loop, + * from the perspective of the root caller (if it crossed any processes + * via Binder calls). The value is 0 if the root caller wasn't on a Looper + * thread. + */ + public int violationNumThisLoop; + + /** + * The time (in terms of SystemClock.uptimeMillis()) that the + * violation occurred. + */ + public long violationUptimeMillis; + + /** + * The action of the Intent being broadcast to somebody's onReceive + * on this thread right now, or null. + */ + public String broadcastIntentAction; + + /** + * If this is a instance count violation, the number of instances in memory, + * else -1. + */ + public long numInstances = -1; + + /** + * Create an uninitialized instance of ViolationInfo + */ + public ViolationInfo() { + crashInfo = null; + policy = 0; + } + + /** + * Create an instance of ViolationInfo initialized from an exception. + */ + public ViolationInfo(Throwable tr, int policy) { + crashInfo = new ApplicationErrorReport.CrashInfo(tr); + violationUptimeMillis = SystemClock.uptimeMillis(); + this.policy = policy; + this.numAnimationsRunning = ValueAnimator.getCurrentAnimationsCount(); + Intent broadcastIntent = ActivityThread.getIntentBeingBroadcast(); + if (broadcastIntent != null) { + broadcastIntentAction = broadcastIntent.getAction(); + } + ThreadSpanState state = sThisThreadSpanState.get(); + if (tr instanceof InstanceCountViolation) { + this.numInstances = ((InstanceCountViolation) tr).mInstances; + } + synchronized (state) { + int spanActiveCount = state.mActiveSize; + if (spanActiveCount > MAX_SPAN_TAGS) { + spanActiveCount = MAX_SPAN_TAGS; + } + if (spanActiveCount != 0) { + this.tags = new String[spanActiveCount]; + Span iter = state.mActiveHead; + int index = 0; + while (iter != null && index < spanActiveCount) { + this.tags[index] = iter.mName; + index++; + iter = iter.mNext; + } + } + } + } + + @Override + public int hashCode() { + int result = 17; + result = 37 * result + crashInfo.stackTrace.hashCode(); + if (numAnimationsRunning != 0) { + result *= 37; + } + if (broadcastIntentAction != null) { + result = 37 * result + broadcastIntentAction.hashCode(); + } + if (tags != null) { + for (String tag : tags) { + result = 37 * result + tag.hashCode(); + } + } + return result; + } + + /** + * Create an instance of ViolationInfo initialized from a Parcel. + */ + public ViolationInfo(Parcel in) { + this(in, false); + } + + /** + * Create an instance of ViolationInfo initialized from a Parcel. + * + * @param unsetGatheringBit if true, the caller is the root caller + * and the gathering penalty should be removed. + */ + public ViolationInfo(Parcel in, boolean unsetGatheringBit) { + crashInfo = new ApplicationErrorReport.CrashInfo(in); + int rawPolicy = in.readInt(); + if (unsetGatheringBit) { + policy = rawPolicy & ~PENALTY_GATHER; + } else { + policy = rawPolicy; + } + durationMillis = in.readInt(); + violationNumThisLoop = in.readInt(); + numAnimationsRunning = in.readInt(); + violationUptimeMillis = in.readLong(); + numInstances = in.readLong(); + broadcastIntentAction = in.readString(); + tags = in.readStringArray(); + } + + /** + * Save a ViolationInfo instance to a parcel. + */ + public void writeToParcel(Parcel dest, int flags) { + crashInfo.writeToParcel(dest, flags); + int start = dest.dataPosition(); + dest.writeInt(policy); + dest.writeInt(durationMillis); + dest.writeInt(violationNumThisLoop); + dest.writeInt(numAnimationsRunning); + dest.writeLong(violationUptimeMillis); + dest.writeLong(numInstances); + dest.writeString(broadcastIntentAction); + dest.writeStringArray(tags); + int total = dest.dataPosition()-start; + if (total > 10*1024) { + Slog.d(TAG, "VIO: policy=" + policy + " dur=" + durationMillis + + " numLoop=" + violationNumThisLoop + + " anim=" + numAnimationsRunning + + " uptime=" + violationUptimeMillis + + " numInst=" + numInstances); + Slog.d(TAG, "VIO: action=" + broadcastIntentAction); + Slog.d(TAG, "VIO: tags=" + Arrays.toString(tags)); + Slog.d(TAG, "VIO: TOTAL BYTES WRITTEN: " + (dest.dataPosition()-start)); + } + } + + + /** + * Dump a ViolationInfo instance to a Printer. + */ + public void dump(Printer pw, String prefix) { + crashInfo.dump(pw, prefix); + pw.println(prefix + "policy: " + policy); + if (durationMillis != -1) { + pw.println(prefix + "durationMillis: " + durationMillis); + } + if (numInstances != -1) { + pw.println(prefix + "numInstances: " + numInstances); + } + if (violationNumThisLoop != 0) { + pw.println(prefix + "violationNumThisLoop: " + violationNumThisLoop); + } + if (numAnimationsRunning != 0) { + pw.println(prefix + "numAnimationsRunning: " + numAnimationsRunning); + } + pw.println(prefix + "violationUptimeMillis: " + violationUptimeMillis); + if (broadcastIntentAction != null) { + pw.println(prefix + "broadcastIntentAction: " + broadcastIntentAction); + } + if (tags != null) { + int index = 0; + for (String tag : tags) { + pw.println(prefix + "tag[" + (index++) + "]: " + tag); + } + } + } + + } + + // Dummy throwable, for now, since we don't know when or where the + // leaked instances came from. We might in the future, but for + // now we suppress the stack trace because it's useless and/or + // misleading. + private static class InstanceCountViolation extends Throwable { + final Class mClass; + final long mInstances; + final int mLimit; + + private static final StackTraceElement[] FAKE_STACK = { + new StackTraceElement("android.os.StrictMode", "setClassInstanceLimit", + "StrictMode.java", 1) + }; + + public InstanceCountViolation(Class klass, long instances, int limit) { + super(klass.toString() + "; instances=" + instances + "; limit=" + limit); + setStackTrace(FAKE_STACK); + mClass = klass; + mInstances = instances; + mLimit = limit; + } + } + + private static final class InstanceTracker { + private static final HashMap, Integer> sInstanceCounts = + new HashMap, Integer>(); + + private final Class mKlass; + + public InstanceTracker(Object instance) { + mKlass = instance.getClass(); + + synchronized (sInstanceCounts) { + final Integer value = sInstanceCounts.get(mKlass); + final int newValue = value != null ? value + 1 : 1; + sInstanceCounts.put(mKlass, newValue); + } + } + + @Override + protected void finalize() throws Throwable { + try { + synchronized (sInstanceCounts) { + final Integer value = sInstanceCounts.get(mKlass); + if (value != null) { + final int newValue = value - 1; + if (newValue > 0) { + sInstanceCounts.put(mKlass, newValue); + } else { + sInstanceCounts.remove(mKlass); + } + } + } + } finally { + super.finalize(); + } + } + + public static int getInstanceCount(Class klass) { + synchronized (sInstanceCounts) { + final Integer value = sInstanceCounts.get(klass); + return value != null ? value : 0; + } + } + } +} diff --git a/src/main/java/android/os/StrictModeBenchmark.java b/src/main/java/android/os/StrictModeBenchmark.java new file mode 100644 index 0000000..41af382 --- /dev/null +++ b/src/main/java/android/os/StrictModeBenchmark.java @@ -0,0 +1,34 @@ +/* + * 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 android.os; + +import android.os.StrictMode.ThreadPolicy; + +import com.google.caliper.SimpleBenchmark; + +public class StrictModeBenchmark extends SimpleBenchmark { + + private ThreadPolicy mOff = new ThreadPolicy.Builder().build(); + private ThreadPolicy mOn = new ThreadPolicy.Builder().detectAll().build(); + + public void timeToggleThreadPolicy(int reps) { + for (int i = 0; i < reps; i++) { + StrictMode.setThreadPolicy(mOn); + StrictMode.setThreadPolicy(mOff); + } + } +} diff --git a/src/main/java/android/os/SystemClock.java b/src/main/java/android/os/SystemClock.java new file mode 100644 index 0000000..672df6d --- /dev/null +++ b/src/main/java/android/os/SystemClock.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.app.IAlarmManager; +import android.content.Context; +import android.util.Slog; + +/** + * Core timekeeping facilities. + * + *

    Three different clocks are available, and they should not be confused: + * + *

      + *
    • {@link System#currentTimeMillis System.currentTimeMillis()} + * is the standard "wall" clock (time and date) expressing milliseconds + * since the epoch. The wall clock can be set by the user or the phone + * network (see {@link #setCurrentTimeMillis}), so the time may jump + * backwards or forwards unpredictably. This clock should only be used + * when correspondence with real-world dates and times is important, such + * as in a calendar or alarm clock application. Interval or elapsed + * time measurements should use a different clock. If you are using + * System.currentTimeMillis(), consider listening to the + * {@link android.content.Intent#ACTION_TIME_TICK ACTION_TIME_TICK}, + * {@link android.content.Intent#ACTION_TIME_CHANGED ACTION_TIME_CHANGED} + * and {@link android.content.Intent#ACTION_TIMEZONE_CHANGED + * ACTION_TIMEZONE_CHANGED} {@link android.content.Intent Intent} + * broadcasts to find out when the time changes. + * + *

    • {@link #uptimeMillis} is counted in milliseconds since the + * system was booted. This clock stops when the system enters deep + * sleep (CPU off, display dark, device waiting for external input), + * but is not affected by clock scaling, idle, or other power saving + * mechanisms. This is the basis for most interval timing + * such as {@link Thread#sleep(long) Thread.sleep(millls)}, + * {@link Object#wait(long) Object.wait(millis)}, and + * {@link System#nanoTime System.nanoTime()}. This clock is guaranteed + * to be monotonic, and is suitable for interval timing when the + * interval does not span device sleep. Most methods that accept a + * timestamp value currently expect the {@link #uptimeMillis} clock. + * + *

    • {@link #elapsedRealtime} and {@link #elapsedRealtimeNanos} + * return the time since the system was booted, and include deep sleep. + * This clock is guaranteed to be monotonic, and continues to tick even + * when the CPU is in power saving modes, so is the recommend basis + * for general purpose interval timing. + * + *

    + * + * There are several mechanisms for controlling the timing of events: + * + *
      + *
    • Standard functions like {@link Thread#sleep(long) + * Thread.sleep(millis)} and {@link Object#wait(long) Object.wait(millis)} + * are always available. These functions use the {@link #uptimeMillis} + * clock; if the device enters sleep, the remainder of the time will be + * postponed until the device wakes up. These synchronous functions may + * be interrupted with {@link Thread#interrupt Thread.interrupt()}, and + * you must handle {@link InterruptedException}. + * + *

    • {@link #sleep SystemClock.sleep(millis)} is a utility function + * very similar to {@link Thread#sleep(long) Thread.sleep(millis)}, but it + * ignores {@link InterruptedException}. Use this function for delays if + * you do not use {@link Thread#interrupt Thread.interrupt()}, as it will + * preserve the interrupted state of the thread. + * + *

    • The {@link android.os.Handler} class can schedule asynchronous + * callbacks at an absolute or relative time. Handler objects also use the + * {@link #uptimeMillis} clock, and require an {@link android.os.Looper + * event loop} (normally present in any GUI application). + * + *

    • The {@link android.app.AlarmManager} can trigger one-time or + * recurring events which occur even when the device is in deep sleep + * or your application is not running. Events may be scheduled with your + * choice of {@link java.lang.System#currentTimeMillis} (RTC) or + * {@link #elapsedRealtime} (ELAPSED_REALTIME), and cause an + * {@link android.content.Intent} broadcast when they occur. + *

    + */ +public final class SystemClock { + private static final String TAG = "SystemClock"; + + /** + * This class is uninstantiable. + */ + private SystemClock() { + // This space intentionally left blank. + } + + /** + * Waits a given number of milliseconds (of uptimeMillis) before returning. + * Similar to {@link java.lang.Thread#sleep(long)}, but does not throw + * {@link InterruptedException}; {@link Thread#interrupt()} events are + * deferred until the next interruptible operation. Does not return until + * at least the specified number of milliseconds has elapsed. + * + * @param ms to sleep before returning, in milliseconds of uptime. + */ + public static void sleep(long ms) + { + long start = uptimeMillis(); + long duration = ms; + boolean interrupted = false; + do { + try { + Thread.sleep(duration); + } + catch (InterruptedException e) { + interrupted = true; + } + duration = start + ms - uptimeMillis(); + } while (duration > 0); + + if (interrupted) { + // Important: we don't want to quietly eat an interrupt() event, + // so we make sure to re-interrupt the thread so that the next + // call to Thread.sleep() or Object.wait() will be interrupted. + Thread.currentThread().interrupt(); + } + } + + /** + * Sets the current wall time, in milliseconds. Requires the calling + * process to have appropriate permissions. + * + * @return if the clock was successfully set to the specified time. + */ + public static boolean setCurrentTimeMillis(long millis) { + IBinder b = ServiceManager.getService(Context.ALARM_SERVICE); + IAlarmManager mgr = IAlarmManager.Stub.asInterface(b); + if (mgr == null) { + return false; + } + + try { + return mgr.setTime(millis); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to set RTC", e); + } catch (SecurityException e) { + Slog.e(TAG, "Unable to set RTC", e); + } + + return false; + } + + /** + * Returns milliseconds since boot, not counting time spent in deep sleep. + * + * @return milliseconds of non-sleep uptime since boot. + */ + native public static long uptimeMillis(); + + /** + * Returns milliseconds since boot, including time spent in sleep. + * + * @return elapsed milliseconds since boot. + */ + native public static long elapsedRealtime(); + + /** + * Returns nanoseconds since boot, including time spent in sleep. + * + * @return elapsed nanoseconds since boot. + */ + public static native long elapsedRealtimeNanos(); + + /** + * Returns milliseconds running in the current thread. + * + * @return elapsed milliseconds in the thread + */ + public static native long currentThreadTimeMillis(); + + /** + * Returns microseconds running in the current thread. + * + * @return elapsed microseconds in the thread + * + * @hide + */ + public static native long currentThreadTimeMicro(); + + /** + * Returns current wall time in microseconds. + * + * @return elapsed microseconds in wall time + * + * @hide + */ + public static native long currentTimeMicro(); +} diff --git a/src/main/java/android/os/SystemClock_Delegate.java b/src/main/java/android/os/SystemClock_Delegate.java new file mode 100644 index 0000000..5f0d98b --- /dev/null +++ b/src/main/java/android/os/SystemClock_Delegate.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2010 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 android.os; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.os.SystemClock + * + * Through the layoutlib_create tool, the original native methods of SystemClock have been replaced + * by calls to methods of the same name in this delegate class. + * + * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager} + * around to map int to instance of the delegate. + * + */ +public class SystemClock_Delegate { + private static long sBootTime = System.currentTimeMillis(); + private static long sBootTimeNano = System.nanoTime(); + + /** + * Returns milliseconds since boot, not counting time spent in deep sleep. + * Note: This value may get reset occasionally (before it would + * otherwise wrap around). + * + * @return milliseconds of non-sleep uptime since boot. + */ + @LayoutlibDelegate + /*package*/ static long uptimeMillis() { + return System.currentTimeMillis() - sBootTime; + } + + /** + * Returns milliseconds since boot, including time spent in sleep. + * + * @return elapsed milliseconds since boot. + */ + @LayoutlibDelegate + /*package*/ static long elapsedRealtime() { + return System.currentTimeMillis() - sBootTime; + } + + /** + * Returns nanoseconds since boot, including time spent in sleep. + * + * @return elapsed nanoseconds since boot. + */ + @LayoutlibDelegate + /*package*/ static long elapsedRealtimeNanos() { + return System.nanoTime() - sBootTimeNano; + } + + /** + * Returns milliseconds running in the current thread. + * + * @return elapsed milliseconds in the thread + */ + @LayoutlibDelegate + /*package*/ static long currentThreadTimeMillis() { + return System.currentTimeMillis(); + } + + /** + * Returns microseconds running in the current thread. + * + * @return elapsed microseconds in the thread + * + * @hide + */ + @LayoutlibDelegate + /*package*/ static long currentThreadTimeMicro() { + return System.currentTimeMillis() * 1000; + } + + /** + * Returns current wall time in microseconds. + * + * @return elapsed microseconds in wall time + * + * @hide + */ + @LayoutlibDelegate + /*package*/ static long currentTimeMicro() { + return elapsedRealtime() * 1000; + } +} diff --git a/src/main/java/android/os/SystemProperties.java b/src/main/java/android/os/SystemProperties.java new file mode 100644 index 0000000..1479035 --- /dev/null +++ b/src/main/java/android/os/SystemProperties.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2006 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 android.os; + +import java.util.ArrayList; + + +/** + * Gives access to the system properties store. The system properties + * store contains a list of string key-value pairs. + * + * {@hide} + */ +public class SystemProperties +{ + public static final int PROP_NAME_MAX = 31; + public static final int PROP_VALUE_MAX = 91; + + private static final ArrayList sChangeCallbacks = new ArrayList(); + + private static native String native_get(String key); + private static native String native_get(String key, String def); + private static native int native_get_int(String key, int def); + private static native long native_get_long(String key, long def); + private static native boolean native_get_boolean(String key, boolean def); + private static native void native_set(String key, String def); + private static native void native_add_change_callback(); + + /** + * Get the value for the given key. + * @return an empty string if the key isn't found + * @throws IllegalArgumentException if the key exceeds 32 characters + */ + public static String get(String key) { + if (key.length() > PROP_NAME_MAX) { + throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX); + } + return native_get(key); + } + + /** + * Get the value for the given key. + * @return if the key isn't found, return def if it isn't null, or an empty string otherwise + * @throws IllegalArgumentException if the key exceeds 32 characters + */ + public static String get(String key, String def) { + if (key.length() > PROP_NAME_MAX) { + throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX); + } + return native_get(key, def); + } + + /** + * Get the value for the given key, and return as an integer. + * @param key the key to lookup + * @param def a default value to return + * @return the key parsed as an integer, or def if the key isn't found or + * cannot be parsed + * @throws IllegalArgumentException if the key exceeds 32 characters + */ + public static int getInt(String key, int def) { + if (key.length() > PROP_NAME_MAX) { + throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX); + } + return native_get_int(key, def); + } + + /** + * Get the value for the given key, and return as a long. + * @param key the key to lookup + * @param def a default value to return + * @return the key parsed as a long, or def if the key isn't found or + * cannot be parsed + * @throws IllegalArgumentException if the key exceeds 32 characters + */ + public static long getLong(String key, long def) { + if (key.length() > PROP_NAME_MAX) { + throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX); + } + return native_get_long(key, def); + } + + /** + * Get the value for the given key, returned as a boolean. + * Values 'n', 'no', '0', 'false' or 'off' are considered false. + * Values 'y', 'yes', '1', 'true' or 'on' are considered true. + * (case sensitive). + * If the key does not exist, or has any other value, then the default + * result is returned. + * @param key the key to lookup + * @param def a default value to return + * @return the key parsed as a boolean, or def if the key isn't found or is + * not able to be parsed as a boolean. + * @throws IllegalArgumentException if the key exceeds 32 characters + */ + public static boolean getBoolean(String key, boolean def) { + if (key.length() > PROP_NAME_MAX) { + throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX); + } + return native_get_boolean(key, def); + } + + /** + * Set the value for the given key. + * @throws IllegalArgumentException if the key exceeds 32 characters + * @throws IllegalArgumentException if the value exceeds 92 characters + */ + public static void set(String key, String val) { + if (key.length() > PROP_NAME_MAX) { + throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX); + } + if (val != null && val.length() > PROP_VALUE_MAX) { + throw new IllegalArgumentException("val.length > " + + PROP_VALUE_MAX); + } + native_set(key, val); + } + + public static void addChangeCallback(Runnable callback) { + synchronized (sChangeCallbacks) { + if (sChangeCallbacks.size() == 0) { + native_add_change_callback(); + } + sChangeCallbacks.add(callback); + } + } + + static void callChangeCallbacks() { + synchronized (sChangeCallbacks) { + //Log.i("foo", "Calling " + sChangeCallbacks.size() + " change callbacks!"); + if (sChangeCallbacks.size() == 0) { + return; + } + ArrayList callbacks = new ArrayList(sChangeCallbacks); + for (int i=0; i properties = Bridge.getPlatformProperties(); + String value = properties.get(key); + if (value != null) { + return value; + } + + return def; + } + @LayoutlibDelegate + /*package*/ static int native_get_int(String key, int def) { + Map properties = Bridge.getPlatformProperties(); + String value = properties.get(key); + if (value != null) { + return Integer.decode(value); + } + + return def; + } + + @LayoutlibDelegate + /*package*/ static long native_get_long(String key, long def) { + Map properties = Bridge.getPlatformProperties(); + String value = properties.get(key); + if (value != null) { + return Long.decode(value); + } + + return def; + } + + /** + * Values 'n', 'no', '0', 'false' or 'off' are considered false. + * Values 'y', 'yes', '1', 'true' or 'on' are considered true. + */ + @LayoutlibDelegate + /*package*/ static boolean native_get_boolean(String key, boolean def) { + Map properties = Bridge.getPlatformProperties(); + String value = properties.get(key); + + if ("n".equals(value) || "no".equals(value) || "0".equals(value) || "false".equals(value) + || "off".equals(value)) { + return false; + } + //noinspection SimplifiableIfStatement + if ("y".equals(value) || "yes".equals(value) || "1".equals(value) || "true".equals(value) + || "on".equals(value)) { + return true; + } + + return def; + } + + @LayoutlibDelegate + /*package*/ static void native_set(String key, String def) { + Map properties = Bridge.getPlatformProperties(); + properties.put(key, def); + } + + @LayoutlibDelegate + /*package*/ static void native_add_change_callback() { + // pass. + } +} diff --git a/src/main/java/android/os/SystemService.java b/src/main/java/android/os/SystemService.java new file mode 100644 index 0000000..41e7546 --- /dev/null +++ b/src/main/java/android/os/SystemService.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2006 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 android.os; + +import com.google.android.collect.Maps; + +import java.util.HashMap; +import java.util.concurrent.TimeoutException; + +/** + * Controls and utilities for low-level {@code init} services. + * + * @hide + */ +public class SystemService { + + private static HashMap sStates = Maps.newHashMap(); + + /** + * State of a known {@code init} service. + */ + public enum State { + RUNNING("running"), + STOPPING("stopping"), + STOPPED("stopped"), + RESTARTING("restarting"); + + State(String state) { + sStates.put(state, this); + } + } + + private static Object sPropertyLock = new Object(); + + static { + SystemProperties.addChangeCallback(new Runnable() { + @Override + public void run() { + synchronized (sPropertyLock) { + sPropertyLock.notifyAll(); + } + } + }); + } + + /** Request that the init daemon start a named service. */ + public static void start(String name) { + SystemProperties.set("ctl.start", name); + } + + /** Request that the init daemon stop a named service. */ + public static void stop(String name) { + SystemProperties.set("ctl.stop", name); + } + + /** Request that the init daemon restart a named service. */ + public static void restart(String name) { + SystemProperties.set("ctl.restart", name); + } + + /** + * Return current state of given service. + */ + public static State getState(String service) { + final String rawState = SystemProperties.get("init.svc." + service); + final State state = sStates.get(rawState); + if (state != null) { + return state; + } else { + return State.STOPPED; + } + } + + /** + * Check if given service is {@link State#STOPPED}. + */ + public static boolean isStopped(String service) { + return State.STOPPED.equals(getState(service)); + } + + /** + * Check if given service is {@link State#RUNNING}. + */ + public static boolean isRunning(String service) { + return State.RUNNING.equals(getState(service)); + } + + /** + * Wait until given service has entered specific state. + */ + public static void waitForState(String service, State state, long timeoutMillis) + throws TimeoutException { + final long endMillis = SystemClock.elapsedRealtime() + timeoutMillis; + while (true) { + synchronized (sPropertyLock) { + final State currentState = getState(service); + if (state.equals(currentState)) { + return; + } + + if (SystemClock.elapsedRealtime() >= endMillis) { + throw new TimeoutException("Service " + service + " currently " + currentState + + "; waited " + timeoutMillis + "ms for " + state); + } + + try { + sPropertyLock.wait(timeoutMillis); + } catch (InterruptedException e) { + } + } + } + } + + /** + * Wait until any of given services enters {@link State#STOPPED}. + */ + public static void waitForAnyStopped(String... services) { + while (true) { + synchronized (sPropertyLock) { + for (String service : services) { + if (State.STOPPED.equals(getState(service))) { + return; + } + } + + try { + sPropertyLock.wait(); + } catch (InterruptedException e) { + } + } + } + } +} diff --git a/src/main/java/android/os/SystemVibrator.java b/src/main/java/android/os/SystemVibrator.java new file mode 100644 index 0000000..c488811 --- /dev/null +++ b/src/main/java/android/os/SystemVibrator.java @@ -0,0 +1,114 @@ +/* + * 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 android.os; + +import android.content.Context; +import android.media.AudioAttributes; +import android.util.Log; + +/** + * Vibrator implementation that controls the main system vibrator. + * + * @hide + */ +public class SystemVibrator extends Vibrator { + private static final String TAG = "Vibrator"; + + private final IVibratorService mService; + private final Binder mToken = new Binder(); + + public SystemVibrator() { + mService = IVibratorService.Stub.asInterface( + ServiceManager.getService("vibrator")); + } + + public SystemVibrator(Context context) { + super(context); + mService = IVibratorService.Stub.asInterface( + ServiceManager.getService("vibrator")); + } + + @Override + public boolean hasVibrator() { + if (mService == null) { + Log.w(TAG, "Failed to vibrate; no vibrator service."); + return false; + } + try { + return mService.hasVibrator(); + } catch (RemoteException e) { + } + return false; + } + + /** + * @hide + */ + @Override + public void vibrate(int uid, String opPkg, long milliseconds, AudioAttributes attributes) { + if (mService == null) { + Log.w(TAG, "Failed to vibrate; no vibrator service."); + return; + } + try { + mService.vibrate(uid, opPkg, milliseconds, usageForAttributes(attributes), mToken); + } catch (RemoteException e) { + Log.w(TAG, "Failed to vibrate.", e); + } + } + + /** + * @hide + */ + @Override + public void vibrate(int uid, String opPkg, long[] pattern, int repeat, + AudioAttributes attributes) { + if (mService == null) { + Log.w(TAG, "Failed to vibrate; no vibrator service."); + return; + } + // catch this here because the server will do nothing. pattern may + // not be null, let that be checked, because the server will drop it + // anyway + if (repeat < pattern.length) { + try { + mService.vibratePattern(uid, opPkg, pattern, repeat, usageForAttributes(attributes), + mToken); + } catch (RemoteException e) { + Log.w(TAG, "Failed to vibrate.", e); + } + } else { + throw new ArrayIndexOutOfBoundsException(); + } + } + + private static int usageForAttributes(AudioAttributes attributes) { + return attributes != null ? attributes.getUsage() : AudioAttributes.USAGE_UNKNOWN; + } + + @Override + public void cancel() { + if (mService == null) { + return; + } + try { + mService.cancelVibrate(mToken); + } catch (RemoteException e) { + Log.w(TAG, "Failed to cancel vibration.", e); + } + } +} diff --git a/src/main/java/android/os/TestHandlerThread.java b/src/main/java/android/os/TestHandlerThread.java new file mode 100644 index 0000000..7e84af3 --- /dev/null +++ b/src/main/java/android/os/TestHandlerThread.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2007 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 android.os; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.MessageQueue.IdleHandler; + +abstract class TestHandlerThread { + private boolean mDone = false; + private boolean mSuccess = false; + private RuntimeException mFailure = null; + private Looper mLooper; + + public abstract void go(); + + public TestHandlerThread() { + } + + public void doTest(long timeout) { + (new LooperThread()).start(); + + synchronized (this) { + long now = System.currentTimeMillis(); + long endTime = now + timeout; + while (!mDone && now < endTime) { + try { + wait(endTime-now); + } + catch (InterruptedException e) { + } + now = System.currentTimeMillis(); + } + } + + mLooper.quit(); + + if (!mDone) { + throw new RuntimeException("test timed out"); + } + if (!mSuccess) { + throw mFailure; + } + } + + public Looper getLooper() { + return mLooper; + } + + public void success() { + synchronized (this) { + mSuccess = true; + quit(); + } + } + + public void failure(RuntimeException failure) { + synchronized (this) { + mSuccess = false; + mFailure = failure; + quit(); + } + } + + class LooperThread extends Thread { + public void run() { + Looper.prepare(); + mLooper = Looper.myLooper(); + go(); + Looper.loop(); + + synchronized (TestHandlerThread.this) { + mDone = true; + if (!mSuccess && mFailure == null) { + mFailure = new RuntimeException("no failure exception set"); + } + TestHandlerThread.this.notifyAll(); + } + } + + } + + private void quit() { + synchronized (this) { + mDone = true; + notifyAll(); + } + } +} diff --git a/src/main/java/android/os/TokenWatcher.java b/src/main/java/android/os/TokenWatcher.java new file mode 100644 index 0000000..9b3a2d6 --- /dev/null +++ b/src/main/java/android/os/TokenWatcher.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2007 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 android.os; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.WeakHashMap; +import java.util.Set; +import android.util.Log; + +/** + * Helper class that helps you use IBinder objects as reference counted + * tokens. IBinders make good tokens because we find out when they are + * removed + * + */ +public abstract class TokenWatcher +{ + /** + * Construct the TokenWatcher + * + * @param h A handler to call {@link #acquired} and {@link #released} + * on. If you don't care, just call it like this, although your thread + * will have to be a Looper thread. + * new TokenWatcher(new Handler()) + * @param tag A debugging tag for this TokenWatcher + */ + public TokenWatcher(Handler h, String tag) + { + mHandler = h; + mTag = tag != null ? tag : "TokenWatcher"; + } + + /** + * Called when the number of active tokens goes from 0 to 1. + */ + public abstract void acquired(); + + /** + * Called when the number of active tokens goes from 1 to 0. + */ + public abstract void released(); + + /** + * Record that this token has been acquired. When acquire is called, and + * the current count is 0, the acquired method is called on the given + * handler. + * + * @param token An IBinder object. If this token has already been acquired, + * no action is taken. + * @param tag A string used by the {@link #dump} method for debugging, + * to see who has references. + */ + public void acquire(IBinder token, String tag) + { + synchronized (mTokens) { + // explicitly checked to avoid bogus sendNotification calls because + // of the WeakHashMap and the GC + int oldSize = mTokens.size(); + + Death d = new Death(token, tag); + try { + token.linkToDeath(d, 0); + } catch (RemoteException e) { + return; + } + mTokens.put(token, d); + + if (oldSize == 0 && !mAcquired) { + sendNotificationLocked(true); + mAcquired = true; + } + } + } + + public void cleanup(IBinder token, boolean unlink) + { + synchronized (mTokens) { + Death d = mTokens.remove(token); + if (unlink && d != null) { + d.token.unlinkToDeath(d, 0); + d.token = null; + } + + if (mTokens.size() == 0 && mAcquired) { + sendNotificationLocked(false); + mAcquired = false; + } + } + } + + public void release(IBinder token) + { + cleanup(token, true); + } + + public boolean isAcquired() + { + synchronized (mTokens) { + return mAcquired; + } + } + + public void dump() + { + ArrayList a = dumpInternal(); + for (String s : a) { + Log.i(mTag, s); + } + } + + public void dump(PrintWriter pw) { + ArrayList a = dumpInternal(); + for (String s : a) { + pw.println(s); + } + } + + private ArrayList dumpInternal() { + ArrayList a = new ArrayList(); + synchronized (mTokens) { + Set keys = mTokens.keySet(); + a.add("Token count: " + mTokens.size()); + int i = 0; + for (IBinder b: keys) { + a.add("[" + i + "] " + mTokens.get(b).tag + " - " + b); + i++; + } + } + return a; + } + + private Runnable mNotificationTask = new Runnable() { + public void run() + { + int value; + synchronized (mTokens) { + value = mNotificationQueue; + mNotificationQueue = -1; + } + if (value == 1) { + acquired(); + } + else if (value == 0) { + released(); + } + } + }; + + private void sendNotificationLocked(boolean on) + { + int value = on ? 1 : 0; + if (mNotificationQueue == -1) { + // empty + mNotificationQueue = value; + mHandler.post(mNotificationTask); + } + else if (mNotificationQueue != value) { + // it's a pair, so cancel it + mNotificationQueue = -1; + mHandler.removeCallbacks(mNotificationTask); + } + // else, same so do nothing -- maybe we should warn? + } + + private class Death implements IBinder.DeathRecipient + { + IBinder token; + String tag; + + Death(IBinder token, String tag) + { + this.token = token; + this.tag = tag; + } + + public void binderDied() + { + cleanup(token, false); + } + + protected void finalize() throws Throwable + { + try { + if (token != null) { + Log.w(mTag, "cleaning up leaked reference: " + tag); + release(token); + } + } + finally { + super.finalize(); + } + } + } + + private WeakHashMap mTokens = new WeakHashMap(); + private Handler mHandler; + private String mTag; + private int mNotificationQueue = -1; + private volatile boolean mAcquired = false; +} diff --git a/src/main/java/android/os/Trace.java b/src/main/java/android/os/Trace.java new file mode 100644 index 0000000..31b5849 --- /dev/null +++ b/src/main/java/android/os/Trace.java @@ -0,0 +1,292 @@ +/* + * 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 android.os; + +/** + * Writes trace events to the system trace buffer. These trace events can be + * collected and visualized using the Systrace tool. + * + *

    This tracing mechanism is independent of the method tracing mechanism + * offered by {@link Debug#startMethodTracing}. In particular, it enables + * tracing of events that occur across multiple processes. + *

    For information about using the Systrace tool, read Analyzing Display and Performance + * with Systrace. + */ +public final class Trace { + /* + * Writes trace events to the kernel trace buffer. These trace events can be + * collected using the "atrace" program for offline analysis. + */ + + private static final String TAG = "Trace"; + + // These tags must be kept in sync with system/core/include/cutils/trace.h. + // They should also be added to frameworks/native/cmds/atrace/atrace.cpp. + /** @hide */ + public static final long TRACE_TAG_NEVER = 0; + /** @hide */ + public static final long TRACE_TAG_ALWAYS = 1L << 0; + /** @hide */ + public static final long TRACE_TAG_GRAPHICS = 1L << 1; + /** @hide */ + public static final long TRACE_TAG_INPUT = 1L << 2; + /** @hide */ + public static final long TRACE_TAG_VIEW = 1L << 3; + /** @hide */ + public static final long TRACE_TAG_WEBVIEW = 1L << 4; + /** @hide */ + public static final long TRACE_TAG_WINDOW_MANAGER = 1L << 5; + /** @hide */ + public static final long TRACE_TAG_ACTIVITY_MANAGER = 1L << 6; + /** @hide */ + public static final long TRACE_TAG_SYNC_MANAGER = 1L << 7; + /** @hide */ + public static final long TRACE_TAG_AUDIO = 1L << 8; + /** @hide */ + public static final long TRACE_TAG_VIDEO = 1L << 9; + /** @hide */ + public static final long TRACE_TAG_CAMERA = 1L << 10; + /** @hide */ + public static final long TRACE_TAG_HAL = 1L << 11; + /** @hide */ + public static final long TRACE_TAG_APP = 1L << 12; + /** @hide */ + public static final long TRACE_TAG_RESOURCES = 1L << 13; + /** @hide */ + public static final long TRACE_TAG_DALVIK = 1L << 14; + /** @hide */ + public static final long TRACE_TAG_RS = 1L << 15; + /** @hide */ + public static final long TRACE_TAG_BIONIC = 1L << 16; + /** @hide */ + public static final long TRACE_TAG_POWER = 1L << 17; + + private static final long TRACE_TAG_NOT_READY = 1L << 63; + private static final int MAX_SECTION_NAME_LEN = 127; + + // Must be volatile to avoid word tearing. + private static volatile long sEnabledTags = TRACE_TAG_NOT_READY; + + private static native long nativeGetEnabledTags(); + private static native void nativeTraceCounter(long tag, String name, int value); + private static native void nativeTraceBegin(long tag, String name); + private static native void nativeTraceEnd(long tag); + private static native void nativeAsyncTraceBegin(long tag, String name, int cookie); + private static native void nativeAsyncTraceEnd(long tag, String name, int cookie); + private static native void nativeSetAppTracingAllowed(boolean allowed); + private static native void nativeSetTracingEnabled(boolean allowed); + + static { + // We configure two separate change callbacks, one in Trace.cpp and one here. The + // native callback reads the tags from the system property, and this callback + // reads the value that the native code retrieved. It's essential that the native + // callback executes first. + // + // The system provides ordering through a priority level. Callbacks made through + // SystemProperties.addChangeCallback currently have a negative priority, while + // our native code is using a priority of zero. + SystemProperties.addChangeCallback(new Runnable() { + @Override public void run() { + cacheEnabledTags(); + } + }); + } + + private Trace() { + } + + /** + * Caches a copy of the enabled-tag bits. The "master" copy is held by the native code, + * and comes from the PROPERTY_TRACE_TAG_ENABLEFLAGS property. + *

    + * If the native code hasn't yet read the property, we will cause it to do one-time + * initialization. We don't want to do this during class init, because this class is + * preloaded, so all apps would be stuck with whatever the zygote saw. (The zygote + * doesn't see the system-property update broadcasts.) + *

    + * We want to defer initialization until the first use by an app, post-zygote. + *

    + * We're okay if multiple threads call here simultaneously -- the native state is + * synchronized, and sEnabledTags is volatile (prevents word tearing). + */ + private static long cacheEnabledTags() { + long tags = nativeGetEnabledTags(); + sEnabledTags = tags; + return tags; + } + + /** + * Returns true if a trace tag is enabled. + * + * @param traceTag The trace tag to check. + * @return True if the trace tag is valid. + * + * @hide + */ + public static boolean isTagEnabled(long traceTag) { + long tags = sEnabledTags; + if (tags == TRACE_TAG_NOT_READY) { + tags = cacheEnabledTags(); + } + return (tags & traceTag) != 0; + } + + /** + * Writes trace message to indicate the value of a given counter. + * + * @param traceTag The trace tag. + * @param counterName The counter name to appear in the trace. + * @param counterValue The counter value. + * + * @hide + */ + public static void traceCounter(long traceTag, String counterName, int counterValue) { + if (isTagEnabled(traceTag)) { + nativeTraceCounter(traceTag, counterName, counterValue); + } + } + + /** + * Set whether application tracing is allowed for this process. This is intended to be set + * once at application start-up time based on whether the application is debuggable. + * + * @hide + */ + public static void setAppTracingAllowed(boolean allowed) { + nativeSetAppTracingAllowed(allowed); + + // Setting whether app tracing is allowed may change the tags, so we update the cached + // tags here. + cacheEnabledTags(); + } + + /** + * Set whether tracing is enabled in this process. Tracing is disabled shortly after Zygote + * initializes and re-enabled after processes fork from Zygote. This is done because Zygote + * has no way to be notified about changes to the tracing tags, and if Zygote ever reads and + * caches the tracing tags, forked processes will inherit those stale tags. + * + * @hide + */ + public static void setTracingEnabled(boolean enabled) { + nativeSetTracingEnabled(enabled); + + // Setting whether tracing is enabled may change the tags, so we update the cached tags + // here. + cacheEnabledTags(); + } + + /** + * Writes a trace message to indicate that a given section of code has + * begun. Must be followed by a call to {@link #traceEnd} using the same + * tag. + * + * @param traceTag The trace tag. + * @param methodName The method name to appear in the trace. + * + * @hide + */ + public static void traceBegin(long traceTag, String methodName) { + if (isTagEnabled(traceTag)) { + nativeTraceBegin(traceTag, methodName); + } + } + + /** + * Writes a trace message to indicate that the current method has ended. + * Must be called exactly once for each call to {@link #traceBegin} using the same tag. + * + * @param traceTag The trace tag. + * + * @hide + */ + public static void traceEnd(long traceTag) { + if (isTagEnabled(traceTag)) { + nativeTraceEnd(traceTag); + } + } + + /** + * Writes a trace message to indicate that a given section of code has + * begun. Must be followed by a call to {@link #asyncTraceEnd} using the same + * tag. Unlike {@link #traceBegin(long, String)} and {@link #traceEnd(long)}, + * asynchronous events do not need to be nested. The name and cookie used to + * begin an event must be used to end it. + * + * @param traceTag The trace tag. + * @param methodName The method name to appear in the trace. + * @param cookie Unique identifier for distinguishing simultaneous events + * + * @hide + */ + public static void asyncTraceBegin(long traceTag, String methodName, int cookie) { + if (isTagEnabled(traceTag)) { + nativeAsyncTraceBegin(traceTag, methodName, cookie); + } + } + + /** + * Writes a trace message to indicate that the current method has ended. + * Must be called exactly once for each call to {@link #asyncTraceBegin(long, String, int)} + * using the same tag, name and cookie. + * + * @param traceTag The trace tag. + * @param methodName The method name to appear in the trace. + * @param cookie Unique identifier for distinguishing simultaneous events + * + * @hide + */ + public static void asyncTraceEnd(long traceTag, String methodName, int cookie) { + if (isTagEnabled(traceTag)) { + nativeAsyncTraceEnd(traceTag, methodName, cookie); + } + } + + /** + * Writes a trace message to indicate that a given section of code has begun. This call must + * be followed by a corresponding call to {@link #endSection()} on the same thread. + * + *

    At this time the vertical bar character '|', newline character '\n', and + * null character '\0' are used internally by the tracing mechanism. If sectionName contains + * these characters they will be replaced with a space character in the trace. + * + * @param sectionName The name of the code section to appear in the trace. This may be at + * most 127 Unicode code units long. + */ + public static void beginSection(String sectionName) { + if (isTagEnabled(TRACE_TAG_APP)) { + if (sectionName.length() > MAX_SECTION_NAME_LEN) { + throw new IllegalArgumentException("sectionName is too long"); + } + nativeTraceBegin(TRACE_TAG_APP, sectionName); + } + } + + /** + * Writes a trace message to indicate that a given section of code has ended. This call must + * be preceeded by a corresponding call to {@link #beginSection(String)}. Calling this method + * will mark the end of the most recently begun section of code, so care must be taken to + * ensure that beginSection / endSection pairs are properly nested and called from the same + * thread. + */ + public static void endSection() { + if (isTagEnabled(TRACE_TAG_APP)) { + nativeTraceEnd(TRACE_TAG_APP); + } + } +} diff --git a/src/main/java/android/os/TraceTest.java b/src/main/java/android/os/TraceTest.java new file mode 100644 index 0000000..7a788ee --- /dev/null +++ b/src/main/java/android/os/TraceTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.os.Debug; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.SmallTest; +import android.test.suitebuilder.annotation.Suppress; +import android.util.Log; + +/** + * This class is used to test the native tracing support. Run this test + * while tracing on the emulator and then run traceview to view the trace. + */ +public class TraceTest extends AndroidTestCase +{ + private static final String TAG = "TraceTest"; + private int eMethodCalls = 0; + private int fMethodCalls = 0; + private int gMethodCalls = 0; + + @SmallTest + public void testNativeTracingFromJava() + { + long start = System.currentTimeMillis(); + Debug.startNativeTracing(); + //nativeMethod(); + int count = 0; + for (int ii = 0; ii < 20; ii++) { + count = eMethod(); + } + Debug.stopNativeTracing(); + long end = System.currentTimeMillis(); + long elapsed = end - start; + Log.i(TAG, "elapsed millis: " + elapsed); + Log.i(TAG, "eMethod calls: " + eMethodCalls + + " fMethod calls: " + fMethodCalls + + " gMethod calls: " + gMethodCalls); + } + + // This should not run in the automated suite. + @Suppress + public void disableTestNativeTracingFromC() + { + long start = System.currentTimeMillis(); + nativeMethodAndStartTracing(); + long end = System.currentTimeMillis(); + long elapsed = end - start; + Log.i(TAG, "elapsed millis: " + elapsed); + } + + native void nativeMethod(); + native void nativeMethodAndStartTracing(); + + @LargeTest + public void testMethodTracing() + { + long start = System.currentTimeMillis(); + Debug.startMethodTracing("traceTest"); + topMethod(); + Debug.stopMethodTracing(); + long end = System.currentTimeMillis(); + long elapsed = end - start; + Log.i(TAG, "elapsed millis: " + elapsed); + } + + private void topMethod() { + aMethod(); + bMethod(); + cMethod(); + dMethod(5); + + Thread t1 = new aThread(); + t1.start(); + Thread t2 = new aThread(); + t2.start(); + Thread t3 = new aThread(); + t3.start(); + try { + t1.join(); + t2.join(); + t3.join(); + } catch (InterruptedException e) { + } + } + + private class aThread extends Thread { + @Override + public void run() { + aMethod(); + bMethod(); + cMethod(); + } + } + + /** Calls other methods to make some interesting trace data. + * + * @return a meaningless value + */ + private int aMethod() { + int count = 0; + for (int ii = 0; ii < 6; ii++) { + count += bMethod(); + } + for (int ii = 0; ii < 5; ii++) { + count += cMethod(); + } + for (int ii = 0; ii < 4; ii++) { + count += dMethod(ii); + } + return count; + } + + /** Calls another method to make some interesting trace data. + * + * @return a meaningless value + */ + private int bMethod() { + int count = 0; + for (int ii = 0; ii < 4; ii++) { + count += cMethod(); + } + return count; + } + + /** Executes a simple loop to make some interesting trace data. + * + * @return a meaningless value + */ + private int cMethod() { + int count = 0; + for (int ii = 0; ii < 1000; ii++) { + count += ii; + } + return count; + } + + /** Calls itself recursively to make some interesting trace data. + * + * @return a meaningless value + */ + private int dMethod(int level) { + int count = 0; + if (level > 0) { + count = dMethod(level - 1); + } + for (int ii = 0; ii < 100; ii++) { + count += ii; + } + if (level == 0) { + return count; + } + return dMethod(level - 1); + } + + public int eMethod() { + eMethodCalls += 1; + int count = fMethod(); + count += gMethod(3); + return count; + } + + public int fMethod() { + fMethodCalls += 1; + int count = 0; + for (int ii = 0; ii < 10; ii++) { + count += ii; + } + return count; + } + + public int gMethod(int level) { + gMethodCalls += 1; + int count = level; + if (level > 1) + count += gMethod(level - 1); + return count; + } + + /* + * This causes the native shared library to be loaded when the + * class is first used. The library is only loaded once, even if + * multiple classes include this line. + * + * The library must be in java.library.path, which is derived from + * LD_LIBRARY_PATH. The actual library name searched for will be + * "libtrace_test.so" under Linux, but may be different on other + * platforms. + */ + static { + Log.i(TAG, "Loading trace_test native library..."); + try { + System.loadLibrary("trace_test"); + Log.i(TAG, "Successfully loaded trace_test native library"); + } + catch (UnsatisfiedLinkError ule) { + Log.w(TAG, "Could not load trace_test native library"); + } + } +} diff --git a/src/main/java/android/os/TransactionTooLargeException.java b/src/main/java/android/os/TransactionTooLargeException.java new file mode 100644 index 0000000..25f09e8 --- /dev/null +++ b/src/main/java/android/os/TransactionTooLargeException.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2011 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 android.os; +import android.os.RemoteException; + +/** + * The Binder transaction failed because it was too large. + *

    + * During a remote procedure call, the arguments and the return value of the call + * are transferred as {@link Parcel} objects stored in the Binder transaction buffer. + * If the arguments or the return value are too large to fit in the transaction buffer, + * then the call will fail and {@link TransactionTooLargeException} will be thrown. + *

    + * The Binder transaction buffer has a limited fixed size, currently 1Mb, which + * is shared by all transactions in progress for the process. Consequently this + * exception can be thrown when there are many transactions in progress even when + * most of the individual transactions are of moderate size. + *

    + * There are two possible outcomes when a remote procedure call throws + * {@link TransactionTooLargeException}. Either the client was unable to send + * its request to the service (most likely if the arguments were too large to fit in + * the transaction buffer), or the service was unable to send its response back + * to the client (most likely if the return value was too large to fit + * in the transaction buffer). It is not possible to tell which of these outcomes + * actually occurred. The client should assume that a partial failure occurred. + *

    + * The key to avoiding {@link TransactionTooLargeException} is to keep all + * transactions relatively small. Try to minimize the amount of memory needed to create + * a {@link Parcel} for the arguments and the return value of the remote procedure call. + * Avoid transferring huge arrays of strings or large bitmaps. + * If possible, try to break up big requests into smaller pieces. + *

    + * If you are implementing a service, it may help to impose size or complexity + * contraints on the queries that clients can perform. For example, if the result set + * could become large, then don't allow the client to request more than a few records + * at a time. Alternately, instead of returning all of the available data all at once, + * return the essential information first and make the client ask for additional information + * later as needed. + *

    + */ +public class TransactionTooLargeException extends RemoteException { + public TransactionTooLargeException() { + super(); + } +} diff --git a/src/main/java/android/os/UEventObserver.java b/src/main/java/android/os/UEventObserver.java new file mode 100644 index 0000000..9dbfd50 --- /dev/null +++ b/src/main/java/android/os/UEventObserver.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2008 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 android.os; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * UEventObserver is an abstract class that receives UEvent's from the kernel.

    + * + * Subclass UEventObserver, implementing onUEvent(UEvent event), then call + * startObserving() with a match string. The UEvent thread will then call your + * onUEvent() method when a UEvent occurs that contains your match string.

    + * + * Call stopObserving() to stop receiving UEvent's.

    + * + * There is only one UEvent thread per process, even if that process has + * multiple UEventObserver subclass instances. The UEvent thread starts when + * the startObserving() is called for the first time in that process. Once + * started the UEvent thread will not stop (although it can stop notifying + * UEventObserver's via stopObserving()).

    + * + * @hide +*/ +public abstract class UEventObserver { + private static final String TAG = "UEventObserver"; + private static final boolean DEBUG = false; + + private static UEventThread sThread; + + private static native void nativeSetup(); + private static native String nativeWaitForNextEvent(); + private static native void nativeAddMatch(String match); + private static native void nativeRemoveMatch(String match); + + public UEventObserver() { + } + + @Override + protected void finalize() throws Throwable { + try { + stopObserving(); + } finally { + super.finalize(); + } + } + + private static UEventThread getThread() { + synchronized (UEventObserver.class) { + if (sThread == null) { + sThread = new UEventThread(); + sThread.start(); + } + return sThread; + } + } + + private static UEventThread peekThread() { + synchronized (UEventObserver.class) { + return sThread; + } + } + + /** + * Begin observation of UEvent's.

    + * This method will cause the UEvent thread to start if this is the first + * invocation of startObserving in this process.

    + * Once called, the UEvent thread will call onUEvent() when an incoming + * UEvent matches the specified string.

    + * This method can be called multiple times to register multiple matches. + * Only one call to stopObserving is required even with multiple registered + * matches. + * + * @param match A substring of the UEvent to match. Try to be as specific + * as possible to avoid incurring unintended additional cost from processing + * irrelevant messages. Netlink messages can be moderately high bandwidth and + * are expensive to parse. For example, some devices may send one netlink message + * for each vsync period. + */ + public final void startObserving(String match) { + if (match == null || match.isEmpty()) { + throw new IllegalArgumentException("match substring must be non-empty"); + } + + final UEventThread t = getThread(); + t.addObserver(match, this); + } + + /** + * End observation of UEvent's.

    + * This process's UEvent thread will never call onUEvent() on this + * UEventObserver after this call. Repeated calls have no effect. + */ + public final void stopObserving() { + final UEventThread t = getThread(); + if (t != null) { + t.removeObserver(this); + } + } + + /** + * Subclasses of UEventObserver should override this method to handle + * UEvents. + */ + public abstract void onUEvent(UEvent event); + + /** + * Representation of a UEvent. + */ + public static final class UEvent { + // collection of key=value pairs parsed from the uevent message + private final HashMap mMap = new HashMap(); + + public UEvent(String message) { + int offset = 0; + int length = message.length(); + + while (offset < length) { + int equals = message.indexOf('=', offset); + int at = message.indexOf('\0', offset); + if (at < 0) break; + + if (equals > offset && equals < at) { + // key is before the equals sign, and value is after + mMap.put(message.substring(offset, equals), + message.substring(equals + 1, at)); + } + + offset = at + 1; + } + } + + public String get(String key) { + return mMap.get(key); + } + + public String get(String key, String defaultValue) { + String result = mMap.get(key); + return (result == null ? defaultValue : result); + } + + public String toString() { + return mMap.toString(); + } + } + + private static final class UEventThread extends Thread { + /** Many to many mapping of string match to observer. + * Multimap would be better, but not available in android, so use + * an ArrayList where even elements are the String match and odd + * elements the corresponding UEventObserver observer */ + private final ArrayList mKeysAndObservers = new ArrayList(); + + private final ArrayList mTempObserversToSignal = + new ArrayList(); + + public UEventThread() { + super("UEventObserver"); + } + + @Override + public void run() { + nativeSetup(); + + while (true) { + String message = nativeWaitForNextEvent(); + if (message != null) { + if (DEBUG) { + Log.d(TAG, message); + } + sendEvent(message); + } + } + } + + private void sendEvent(String message) { + synchronized (mKeysAndObservers) { + final int N = mKeysAndObservers.size(); + for (int i = 0; i < N; i += 2) { + final String key = (String)mKeysAndObservers.get(i); + if (message.contains(key)) { + final UEventObserver observer = + (UEventObserver)mKeysAndObservers.get(i + 1); + mTempObserversToSignal.add(observer); + } + } + } + + if (!mTempObserversToSignal.isEmpty()) { + final UEvent event = new UEvent(message); + final int N = mTempObserversToSignal.size(); + for (int i = 0; i < N; i++) { + final UEventObserver observer = mTempObserversToSignal.get(i); + observer.onUEvent(event); + } + mTempObserversToSignal.clear(); + } + } + + public void addObserver(String match, UEventObserver observer) { + synchronized (mKeysAndObservers) { + mKeysAndObservers.add(match); + mKeysAndObservers.add(observer); + nativeAddMatch(match); + } + } + + /** Removes every key/value pair where value=observer from mObservers */ + public void removeObserver(UEventObserver observer) { + synchronized (mKeysAndObservers) { + for (int i = 0; i < mKeysAndObservers.size(); ) { + if (mKeysAndObservers.get(i + 1) == observer) { + mKeysAndObservers.remove(i + 1); + final String match = (String)mKeysAndObservers.remove(i); + nativeRemoveMatch(match); + } else { + i += 2; + } + } + } + } + } +} diff --git a/src/main/java/android/os/UpdateLock.java b/src/main/java/android/os/UpdateLock.java new file mode 100644 index 0000000..4060326 --- /dev/null +++ b/src/main/java/android/os/UpdateLock.java @@ -0,0 +1,166 @@ +/* + * 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 android.os; + +import android.content.Context; +import android.util.Log; + +/** + * Advisory wakelock-like mechanism by which processes that should not be interrupted for + * OTA/update purposes can so advise the OS. This is particularly relevant for headless + * or kiosk-like operation. + * + * @hide + */ +public class UpdateLock { + private static final boolean DEBUG = false; + private static final String TAG = "UpdateLock"; + + private static IUpdateLock sService; + private static void checkService() { + if (sService == null) { + sService = IUpdateLock.Stub.asInterface( + ServiceManager.getService(Context.UPDATE_LOCK_SERVICE)); + } + } + + IBinder mToken; + int mCount = 0; + boolean mRefCounted = true; + boolean mHeld = false; + final String mTag; + + /** + * Broadcast Intent action sent when the global update lock state changes, + * i.e. when the first locker acquires an update lock, or when the last + * locker releases theirs. The broadcast is sticky but is sent only to + * registered receivers. + */ + public static final String UPDATE_LOCK_CHANGED = "android.os.UpdateLock.UPDATE_LOCK_CHANGED"; + + /** + * Boolean Intent extra on the UPDATE_LOCK_CHANGED sticky broadcast, indicating + * whether now is an appropriate time to interrupt device activity with an + * update operation. True means that updates are okay right now; false indicates + * that perhaps later would be a better time. + */ + public static final String NOW_IS_CONVENIENT = "nowisconvenient"; + + /** + * Long Intent extra on the UPDATE_LOCK_CHANGED sticky broadcast, marking the + * wall-clock time [in UTC] at which the broadcast was sent. Note that this is + * in the System.currentTimeMillis() time base, which may be non-monotonic especially + * around reboots. + */ + public static final String TIMESTAMP = "timestamp"; + + /** + * Construct an UpdateLock instance. + * @param tag An arbitrary string used to identify this lock instance in dump output. + */ + public UpdateLock(String tag) { + mTag = tag; + mToken = new Binder(); + } + + /** + * Change the refcount behavior of this update lock. + */ + public void setReferenceCounted(boolean isRefCounted) { + if (DEBUG) { + Log.v(TAG, "setting refcounted=" + isRefCounted + " : " + this); + } + mRefCounted = isRefCounted; + } + + /** + * Is this lock currently held? + */ + public boolean isHeld() { + synchronized (mToken) { + return mHeld; + } + } + + /** + * Acquire an update lock. + */ + public void acquire() { + if (DEBUG) { + Log.v(TAG, "acquire() : " + this, new RuntimeException("here")); + } + checkService(); + synchronized (mToken) { + acquireLocked(); + } + } + + private void acquireLocked() { + if (!mRefCounted || mCount++ == 0) { + if (sService != null) { + try { + sService.acquireUpdateLock(mToken, mTag); + } catch (RemoteException e) { + Log.e(TAG, "Unable to contact service to acquire"); + } + } + mHeld = true; + } + } + + /** + * Release this update lock. + */ + public void release() { + if (DEBUG) Log.v(TAG, "release() : " + this, new RuntimeException("here")); + checkService(); + synchronized (mToken) { + releaseLocked(); + } + } + + private void releaseLocked() { + if (!mRefCounted || --mCount == 0) { + if (sService != null) { + try { + sService.releaseUpdateLock(mToken); + } catch (RemoteException e) { + Log.e(TAG, "Unable to contact service to release"); + } + } + mHeld = false; + } + if (mCount < 0) { + throw new RuntimeException("UpdateLock under-locked"); + } + } + + @Override + protected void finalize() throws Throwable { + synchronized (mToken) { + // if mHeld is true, sService must be non-null + if (mHeld) { + Log.wtf(TAG, "UpdateLock finalized while still held"); + try { + sService.releaseUpdateLock(mToken); + } catch (RemoteException e) { + Log.e(TAG, "Unable to contact service to release"); + } + } + } + } +} diff --git a/src/main/java/android/os/UserHandle.java b/src/main/java/android/os/UserHandle.java new file mode 100644 index 0000000..74e064e --- /dev/null +++ b/src/main/java/android/os/UserHandle.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2011 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 android.os; + +import android.annotation.SystemApi; +import android.util.SparseArray; + +import java.io.PrintWriter; +import java.util.HashMap; + +/** + * Representation of a user on the device. + */ +public final class UserHandle implements Parcelable { + /** + * @hide Range of uids allocated for a user. + */ + public static final int PER_USER_RANGE = 100000; + + /** @hide A user id to indicate all users on the device */ + public static final int USER_ALL = -1; + + /** @hide A user handle to indicate all users on the device */ + public static final UserHandle ALL = new UserHandle(USER_ALL); + + /** @hide A user id to indicate the currently active user */ + public static final int USER_CURRENT = -2; + + /** @hide A user handle to indicate the current user of the device */ + public static final UserHandle CURRENT = new UserHandle(USER_CURRENT); + + /** @hide A user id to indicate that we would like to send to the current + * user, but if this is calling from a user process then we will send it + * to the caller's user instead of failing with a security exception */ + public static final int USER_CURRENT_OR_SELF = -3; + + /** @hide A user handle to indicate that we would like to send to the current + * user, but if this is calling from a user process then we will send it + * to the caller's user instead of failing with a security exception */ + public static final UserHandle CURRENT_OR_SELF = new UserHandle(USER_CURRENT_OR_SELF); + + /** @hide An undefined user id */ + public static final int USER_NULL = -10000; + + /** @hide A user id constant to indicate the "owner" user of the device */ + public static final int USER_OWNER = 0; + + /** @hide A user handle to indicate the primary/owner user of the device */ + public static final UserHandle OWNER = new UserHandle(USER_OWNER); + + /** + * @hide Enable multi-user related side effects. Set this to false if + * there are problems with single user use-cases. + */ + public static final boolean MU_ENABLED = true; + + final int mHandle; + + private static final SparseArray userHandles = new SparseArray(); + + /** + * Checks to see if the user id is the same for the two uids, i.e., they belong to the same + * user. + * @hide + */ + public static final boolean isSameUser(int uid1, int uid2) { + return getUserId(uid1) == getUserId(uid2); + } + + /** + * Checks to see if both uids are referring to the same app id, ignoring the user id part of the + * uids. + * @param uid1 uid to compare + * @param uid2 other uid to compare + * @return whether the appId is the same for both uids + * @hide + */ + public static final boolean isSameApp(int uid1, int uid2) { + return getAppId(uid1) == getAppId(uid2); + } + + /** @hide */ + public static final boolean isIsolated(int uid) { + if (uid > 0) { + final int appId = getAppId(uid); + return appId >= Process.FIRST_ISOLATED_UID && appId <= Process.LAST_ISOLATED_UID; + } else { + return false; + } + } + + /** @hide */ + public static boolean isApp(int uid) { + if (uid > 0) { + final int appId = getAppId(uid); + return appId >= Process.FIRST_APPLICATION_UID && appId <= Process.LAST_APPLICATION_UID; + } else { + return false; + } + } + + /** + * Returns the user id for a given uid. + * @hide + */ + public static final int getUserId(int uid) { + if (MU_ENABLED) { + return uid / PER_USER_RANGE; + } else { + return 0; + } + } + + /** @hide */ + public static final int getCallingUserId() { + return getUserId(Binder.getCallingUid()); + } + + /** @hide */ + public static final UserHandle getCallingUserHandle() { + int userId = getUserId(Binder.getCallingUid()); + UserHandle userHandle = userHandles.get(userId); + // Intentionally not synchronized to save time + if (userHandle == null) { + userHandle = new UserHandle(userId); + userHandles.put(userId, userHandle); + } + return userHandle; + } + + /** + * Returns the uid that is composed from the userId and the appId. + * @hide + */ + public static final int getUid(int userId, int appId) { + if (MU_ENABLED) { + return userId * PER_USER_RANGE + (appId % PER_USER_RANGE); + } else { + return appId; + } + } + + /** + * Returns the app id (or base uid) for a given uid, stripping out the user id from it. + * @hide + */ + public static final int getAppId(int uid) { + return uid % PER_USER_RANGE; + } + + /** + * Returns the gid shared between all apps with this userId. + * @hide + */ + public static final int getUserGid(int userId) { + return getUid(userId, Process.SHARED_USER_GID); + } + + /** + * Returns the shared app gid for a given uid or appId. + * @hide + */ + public static final int getSharedAppGid(int id) { + return Process.FIRST_SHARED_APPLICATION_GID + (id % PER_USER_RANGE) + - Process.FIRST_APPLICATION_UID; + } + + /** + * Generate a text representation of the uid, breaking out its individual + * components -- user, app, isolated, etc. + * @hide + */ + public static void formatUid(StringBuilder sb, int uid) { + if (uid < Process.FIRST_APPLICATION_UID) { + sb.append(uid); + } else { + sb.append('u'); + sb.append(getUserId(uid)); + final int appId = getAppId(uid); + if (appId >= Process.FIRST_ISOLATED_UID && appId <= Process.LAST_ISOLATED_UID) { + sb.append('i'); + sb.append(appId - Process.FIRST_ISOLATED_UID); + } else if (appId >= Process.FIRST_APPLICATION_UID) { + sb.append('a'); + sb.append(appId - Process.FIRST_APPLICATION_UID); + } else { + sb.append('s'); + sb.append(appId); + } + } + } + + /** + * Generate a text representation of the uid, breaking out its individual + * components -- user, app, isolated, etc. + * @hide + */ + public static void formatUid(PrintWriter pw, int uid) { + if (uid < Process.FIRST_APPLICATION_UID) { + pw.print(uid); + } else { + pw.print('u'); + pw.print(getUserId(uid)); + final int appId = getAppId(uid); + if (appId >= Process.FIRST_ISOLATED_UID && appId <= Process.LAST_ISOLATED_UID) { + pw.print('i'); + pw.print(appId - Process.FIRST_ISOLATED_UID); + } else if (appId >= Process.FIRST_APPLICATION_UID) { + pw.print('a'); + pw.print(appId - Process.FIRST_APPLICATION_UID); + } else { + pw.print('s'); + pw.print(appId); + } + } + } + + /** + * Returns the user id of the current process + * @return user id of the current process + * @hide + */ + @SystemApi + public static final int myUserId() { + return getUserId(Process.myUid()); + } + + /** + * Returns true if this UserHandle refers to the owner user; false otherwise. + * @return true if this UserHandle refers to the owner user; false otherwise. + * @hide + */ + @SystemApi + public final boolean isOwner() { + return this.equals(OWNER); + } + + /** @hide */ + public UserHandle(int h) { + mHandle = h; + } + + /** + * Returns the userId stored in this UserHandle. + * @hide + */ + @SystemApi + public int getIdentifier() { + return mHandle; + } + + @Override + public String toString() { + return "UserHandle{" + mHandle + "}"; + } + + @Override + public boolean equals(Object obj) { + try { + if (obj != null) { + UserHandle other = (UserHandle)obj; + return mHandle == other.mHandle; + } + } catch (ClassCastException e) { + } + return false; + } + + @Override + public int hashCode() { + return mHandle; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mHandle); + } + + /** + * Write a UserHandle to a Parcel, handling null pointers. Must be + * read with {@link #readFromParcel(Parcel)}. + * + * @param h The UserHandle to be written. + * @param out The Parcel in which the UserHandle will be placed. + * + * @see #readFromParcel(Parcel) + */ + public static void writeToParcel(UserHandle h, Parcel out) { + if (h != null) { + h.writeToParcel(out, 0); + } else { + out.writeInt(USER_NULL); + } + } + + /** + * Read a UserHandle from a Parcel that was previously written + * with {@link #writeToParcel(UserHandle, Parcel)}, returning either + * a null or new object as appropriate. + * + * @param in The Parcel from which to read the UserHandle + * @return Returns a new UserHandle matching the previously written + * object, or null if a null had been written. + * + * @see #writeToParcel(UserHandle, Parcel) + */ + public static UserHandle readFromParcel(Parcel in) { + int h = in.readInt(); + return h != USER_NULL ? new UserHandle(h) : null; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public UserHandle createFromParcel(Parcel in) { + return new UserHandle(in); + } + + public UserHandle[] newArray(int size) { + return new UserHandle[size]; + } + }; + + /** + * Instantiate a new UserHandle from the data in a Parcel that was + * previously written with {@link #writeToParcel(Parcel, int)}. Note that you + * must not use this with data written by + * {@link #writeToParcel(UserHandle, Parcel)} since it is not possible + * to handle a null UserHandle here. + * + * @param in The Parcel containing the previously written UserHandle, + * positioned at the location in the buffer where it was written. + */ + public UserHandle(Parcel in) { + mHandle = in.readInt(); + } +} diff --git a/src/main/java/android/os/UserManager.java b/src/main/java/android/os/UserManager.java new file mode 100644 index 0000000..d124a49 --- /dev/null +++ b/src/main/java/android/os/UserManager.java @@ -0,0 +1,1289 @@ +/* + * 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 android.os; + +import android.annotation.SystemApi; +import android.app.ActivityManager; +import android.app.ActivityManagerNative; +import android.content.Context; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.provider.Settings; +import android.util.Log; +import android.view.WindowManager.LayoutParams; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages users and user details on a multi-user system. + */ +public class UserManager { + + private static String TAG = "UserManager"; + private final IUserManager mService; + private final Context mContext; + + /** + * Specifies if a user is disallowed from adding and removing accounts. + * The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_MODIFY_ACCOUNTS = "no_modify_accounts"; + + /** + * Specifies if a user is disallowed from changing Wi-Fi + * access points. The default value is false. + *

    This restriction has no effect in a managed profile. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CONFIG_WIFI = "no_config_wifi"; + + /** + * Specifies if a user is disallowed from installing applications. + * The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_INSTALL_APPS = "no_install_apps"; + + /** + * Specifies if a user is disallowed from uninstalling applications. + * The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_UNINSTALL_APPS = "no_uninstall_apps"; + + /** + * Specifies if a user is disallowed from turning on location sharing. + * The default value is false. + *

    In a managed profile, location sharing always reflects the primary user's setting, but + * can be overridden and forced off by setting this restriction to true in the managed profile. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_SHARE_LOCATION = "no_share_location"; + + /** + * Specifies if a user is disallowed from enabling the + * "Unknown Sources" setting, that allows installation of apps from unknown sources. + * The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_INSTALL_UNKNOWN_SOURCES = "no_install_unknown_sources"; + + /** + * Specifies if a user is disallowed from configuring bluetooth. + * The default value is false. + *

    This restriction has no effect in a managed profile. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CONFIG_BLUETOOTH = "no_config_bluetooth"; + + /** + * Specifies if a user is disallowed from transferring files over + * USB. This can only be set by device owners and profile owners on the primary user. + * The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_USB_FILE_TRANSFER = "no_usb_file_transfer"; + + /** + * Specifies if a user is disallowed from configuring user + * credentials. The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CONFIG_CREDENTIALS = "no_config_credentials"; + + /** + * When set on the primary user this specifies if the user can remove other users. + * When set on a secondary user, this specifies if the user can remove itself. + * This restriction has no effect on managed profiles. + * The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_REMOVE_USER = "no_remove_user"; + + /** + * Specifies if a user is disallowed from enabling or + * accessing debugging features. The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_DEBUGGING_FEATURES = "no_debugging_features"; + + /** + * Specifies if a user is disallowed from configuring VPN. + * The default value is false. + * This restriction has no effect in a managed profile. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CONFIG_VPN = "no_config_vpn"; + + /** + * Specifies if a user is disallowed from configuring Tethering + * & portable hotspots. This can only be set by device owners and profile owners on the + * primary user. The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CONFIG_TETHERING = "no_config_tethering"; + + /** + * Specifies if a user is disallowed from factory resetting + * from Settings. This can only be set by device owners and profile owners on the primary user. + * The default value is false. + *

    This restriction has no effect on secondary users and managed profiles since only the + * primary user can factory reset the device. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_FACTORY_RESET = "no_factory_reset"; + + /** + * Specifies if a user is disallowed from adding new users and + * profiles. This can only be set by device owners and profile owners on the primary user. + * The default value is false. + *

    This restriction has no effect on secondary users and managed profiles since only the + * primary user can add other users. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_ADD_USER = "no_add_user"; + + /** + * Specifies if a user is disallowed from disabling application + * verification. The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String ENSURE_VERIFY_APPS = "ensure_verify_apps"; + + /** + * Specifies if a user is disallowed from configuring cell + * broadcasts. This can only be set by device owners and profile owners on the primary user. + * The default value is false. + *

    This restriction has no effect on secondary users and managed profiles since only the + * primary user can configure cell broadcasts. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CONFIG_CELL_BROADCASTS = "no_config_cell_broadcasts"; + + /** + * Specifies if a user is disallowed from configuring mobile + * networks. This can only be set by device owners and profile owners on the primary user. + * The default value is false. + *

    This restriction has no effect on secondary users and managed profiles since only the + * primary user can configure mobile networks. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CONFIG_MOBILE_NETWORKS = "no_config_mobile_networks"; + + /** + * Specifies if a user is disallowed from modifying + * applications in Settings or launchers. The following actions will not be allowed when this + * restriction is enabled: + *

  • uninstalling apps
  • + *
  • disabling apps
  • + *
  • clearing app caches
  • + *
  • clearing app data
  • + *
  • force stopping apps
  • + *
  • clearing app defaults
  • + *

    + * The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_APPS_CONTROL = "no_control_apps"; + + /** + * Specifies if a user is disallowed from mounting + * physical external media. This can only be set by device owners and profile owners on the + * primary user. The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_MOUNT_PHYSICAL_MEDIA = "no_physical_media"; + + /** + * Specifies if a user is disallowed from adjusting microphone + * volume. If set, the microphone will be muted. This can only be set by device owners + * and profile owners on the primary user. The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_UNMUTE_MICROPHONE = "no_unmute_microphone"; + + /** + * Specifies if a user is disallowed from adjusting the master + * volume. If set, the master volume will be muted. This can only be set by device owners + * and profile owners on the primary user. The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_ADJUST_VOLUME = "no_adjust_volume"; + + /** + * Specifies that the user is not allowed to make outgoing + * phone calls. Emergency calls are still permitted. + * The default value is false. + *

    This restriction has no effect on managed profiles since call intents are normally + * forwarded to the primary user. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_OUTGOING_CALLS = "no_outgoing_calls"; + + /** + * Specifies that the user is not allowed to send or receive + * SMS messages. The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_SMS = "no_sms"; + + /** + * Specifies that windows besides app windows should not be + * created. This will block the creation of the following types of windows. + *

  • {@link LayoutParams#TYPE_TOAST}
  • + *
  • {@link LayoutParams#TYPE_PHONE}
  • + *
  • {@link LayoutParams#TYPE_PRIORITY_PHONE}
  • + *
  • {@link LayoutParams#TYPE_SYSTEM_ALERT}
  • + *
  • {@link LayoutParams#TYPE_SYSTEM_ERROR}
  • + *
  • {@link LayoutParams#TYPE_SYSTEM_OVERLAY}
  • + * + *

    This can only be set by device owners and profile owners on the primary user. + * The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CREATE_WINDOWS = "no_create_windows"; + + /** + * Specifies if what is copied in the clipboard of this profile can + * be pasted in related profiles. Does not restrict if the clipboard of related profiles can be + * pasted in this profile. + * The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_CROSS_PROFILE_COPY_PASTE = "no_cross_profile_copy_paste"; + + /** + * Specifies if the user is not allowed to use NFC to beam out data from apps. + * The default value is false. + * + *

    Key for user restrictions. + *

    Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_OUTGOING_BEAM = "no_outgoing_beam"; + + /** + * Application restriction key that is used to indicate the pending arrival + * of real restrictions for the app. + * + *

    + * Applications that support restrictions should check for the presence of this key. + * A true value indicates that restrictions may be applied in the near + * future but are not available yet. It is the responsibility of any + * management application that sets this flag to update it when the final + * restrictions are enforced. + * + *

    Key for application restrictions. + *

    Type: Boolean + * @see android.app.admin.DevicePolicyManager#setApplicationRestrictions( + * android.content.ComponentName, String, Bundle) + * @see android.app.admin.DevicePolicyManager#getApplicationRestrictions( + * android.content.ComponentName, String) + */ + public static final String KEY_RESTRICTIONS_PENDING = "restrictions_pending"; + + /** @hide */ + public static final int PIN_VERIFICATION_FAILED_INCORRECT = -3; + /** @hide */ + public static final int PIN_VERIFICATION_FAILED_NOT_SET = -2; + /** @hide */ + public static final int PIN_VERIFICATION_SUCCESS = -1; + + private static UserManager sInstance = null; + + /** @hide */ + public synchronized static UserManager get(Context context) { + if (sInstance == null) { + sInstance = (UserManager) context.getSystemService(Context.USER_SERVICE); + } + return sInstance; + } + + /** @hide */ + public UserManager(Context context, IUserManager service) { + mService = service; + mContext = context; + } + + /** + * Returns whether the system supports multiple users. + * @return true if multiple users can be created by user, false if it is a single user device. + * @hide + */ + public static boolean supportsMultipleUsers() { + return getMaxSupportedUsers() > 1 + && SystemProperties.getBoolean("fw.show_multiuserui", + Resources.getSystem().getBoolean(R.bool.config_enableMultiUserUI)); + } + + /** + * Returns the user handle for the user that the calling process is running on. + * + * @return the user handle of the user making this call. + * @hide + */ + public int getUserHandle() { + return UserHandle.myUserId(); + } + + /** + * Returns the user name of the user making this call. This call is only + * available to applications on the system image; it requires the + * MANAGE_USERS permission. + * @return the user name + */ + public String getUserName() { + try { + return mService.getUserInfo(getUserHandle()).name; + } catch (RemoteException re) { + Log.w(TAG, "Could not get user name", re); + return ""; + } + } + + /** + * Used to determine whether the user making this call is subject to + * teleportations. + * + *

    As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this method can + * now automatically identify goats using advanced goat recognition technology.

    + * + * @return Returns true if the user making this call is a goat. + */ + public boolean isUserAGoat() { + return mContext.getPackageManager() + .isPackageAvailable("com.coffeestainstudios.goatsimulator"); + } + + /** + * Used to check if the user making this call is linked to another user. Linked users may have + * a reduced number of available apps, app restrictions and account restrictions. + * @return whether the user making this call is a linked user + * @hide + */ + public boolean isLinkedUser() { + try { + return mService.isRestricted(); + } catch (RemoteException re) { + Log.w(TAG, "Could not check if user is limited ", re); + return false; + } + } + + /** + * Checks if the calling app is running as a guest user. + * @return whether the caller is a guest user. + * @hide + */ + public boolean isGuestUser() { + UserInfo user = getUserInfo(UserHandle.myUserId()); + return user != null ? user.isGuest() : false; + } + + /** + * Checks if the calling app is running in a managed profile. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * + * @return whether the caller is in a managed profile. + * @hide + */ + @SystemApi + public boolean isManagedProfile() { + UserInfo user = getUserInfo(UserHandle.myUserId()); + return user != null ? user.isManagedProfile() : false; + } + + /** + * Return whether the given user is actively running. This means that + * the user is in the "started" state, not "stopped" -- it is currently + * allowed to run code through scheduled alarms, receiving broadcasts, + * etc. A started user may be either the current foreground user or a + * background user; the result here does not distinguish between the two. + * @param user The user to retrieve the running state for. + */ + public boolean isUserRunning(UserHandle user) { + try { + return ActivityManagerNative.getDefault().isUserRunning( + user.getIdentifier(), false); + } catch (RemoteException e) { + return false; + } + } + + /** + * Return whether the given user is actively running or stopping. + * This is like {@link #isUserRunning(UserHandle)}, but will also return + * true if the user had been running but is in the process of being stopped + * (but is not yet fully stopped, and still running some code). + * @param user The user to retrieve the running state for. + */ + public boolean isUserRunningOrStopping(UserHandle user) { + try { + return ActivityManagerNative.getDefault().isUserRunning( + user.getIdentifier(), true); + } catch (RemoteException e) { + return false; + } + } + + /** + * Returns the UserInfo object describing a specific user. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * @param userHandle the user handle of the user whose information is being requested. + * @return the UserInfo object for a specific user. + * @hide + */ + public UserInfo getUserInfo(int userHandle) { + try { + return mService.getUserInfo(userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Could not get user info", re); + return null; + } + } + + /** + * Returns the user-wide restrictions imposed on this user. + * @return a Bundle containing all the restrictions. + */ + public Bundle getUserRestrictions() { + return getUserRestrictions(Process.myUserHandle()); + } + + /** + * Returns the user-wide restrictions imposed on the user specified by userHandle. + * @param userHandle the UserHandle of the user for whom to retrieve the restrictions. + * @return a Bundle containing all the restrictions. + */ + public Bundle getUserRestrictions(UserHandle userHandle) { + try { + return mService.getUserRestrictions(userHandle.getIdentifier()); + } catch (RemoteException re) { + Log.w(TAG, "Could not get user restrictions", re); + return Bundle.EMPTY; + } + } + + /** + * Sets all the user-wide restrictions for this user. + * Requires the MANAGE_USERS permission. + * @param restrictions the Bundle containing all the restrictions. + * @deprecated use {@link android.app.admin.DevicePolicyManager#addUserRestriction( + * android.content.ComponentName, String)} or + * {@link android.app.admin.DevicePolicyManager#clearUserRestriction( + * android.content.ComponentName, String)} instead. + */ + @Deprecated + public void setUserRestrictions(Bundle restrictions) { + setUserRestrictions(restrictions, Process.myUserHandle()); + } + + /** + * Sets all the user-wide restrictions for the specified user. + * Requires the MANAGE_USERS permission. + * @param restrictions the Bundle containing all the restrictions. + * @param userHandle the UserHandle of the user for whom to set the restrictions. + * @deprecated use {@link android.app.admin.DevicePolicyManager#addUserRestriction( + * android.content.ComponentName, String)} or + * {@link android.app.admin.DevicePolicyManager#clearUserRestriction( + * android.content.ComponentName, String)} instead. + */ + @Deprecated + public void setUserRestrictions(Bundle restrictions, UserHandle userHandle) { + try { + mService.setUserRestrictions(restrictions, userHandle.getIdentifier()); + } catch (RemoteException re) { + Log.w(TAG, "Could not set user restrictions", re); + } + } + + /** + * Sets the value of a specific restriction. + * Requires the MANAGE_USERS permission. + * @param key the key of the restriction + * @param value the value for the restriction + * @deprecated use {@link android.app.admin.DevicePolicyManager#addUserRestriction( + * android.content.ComponentName, String)} or + * {@link android.app.admin.DevicePolicyManager#clearUserRestriction( + * android.content.ComponentName, String)} instead. + */ + @Deprecated + public void setUserRestriction(String key, boolean value) { + Bundle bundle = getUserRestrictions(); + bundle.putBoolean(key, value); + setUserRestrictions(bundle); + } + + /** + * @hide + * Sets the value of a specific restriction on a specific user. + * Requires the MANAGE_USERS permission. + * @param key the key of the restriction + * @param value the value for the restriction + * @param userHandle the user whose restriction is to be changed. + * @deprecated use {@link android.app.admin.DevicePolicyManager#addUserRestriction( + * android.content.ComponentName, String)} or + * {@link android.app.admin.DevicePolicyManager#clearUserRestriction( + * android.content.ComponentName, String)} instead. + */ + @Deprecated + public void setUserRestriction(String key, boolean value, UserHandle userHandle) { + Bundle bundle = getUserRestrictions(userHandle); + bundle.putBoolean(key, value); + setUserRestrictions(bundle, userHandle); + } + + /** + * Returns whether the current user has been disallowed from performing certain actions + * or setting certain settings. + * + * @param restrictionKey The string key representing the restriction. + * @return {@code true} if the current user has the given restriction, {@code false} otherwise. + */ + public boolean hasUserRestriction(String restrictionKey) { + return hasUserRestriction(restrictionKey, Process.myUserHandle()); + } + + /** + * @hide + * Returns whether the given user has been disallowed from performing certain actions + * or setting certain settings. + * @param restrictionKey the string key representing the restriction + * @param userHandle the UserHandle of the user for whom to retrieve the restrictions. + */ + public boolean hasUserRestriction(String restrictionKey, UserHandle userHandle) { + try { + return mService.hasUserRestriction(restrictionKey, + userHandle.getIdentifier()); + } catch (RemoteException re) { + Log.w(TAG, "Could not check user restrictions", re); + return false; + } + } + + /** + * Return the serial number for a user. This is a device-unique + * number assigned to that user; if the user is deleted and then a new + * user created, the new users will not be given the same serial number. + * @param user The user whose serial number is to be retrieved. + * @return The serial number of the given user; returns -1 if the + * given UserHandle does not exist. + * @see #getUserForSerialNumber(long) + */ + public long getSerialNumberForUser(UserHandle user) { + return getUserSerialNumber(user.getIdentifier()); + } + + /** + * Return the user associated with a serial number previously + * returned by {@link #getSerialNumberForUser(UserHandle)}. + * @param serialNumber The serial number of the user that is being + * retrieved. + * @return Return the user associated with the serial number, or null + * if there is not one. + * @see #getSerialNumberForUser(UserHandle) + */ + public UserHandle getUserForSerialNumber(long serialNumber) { + int ident = getUserHandle((int)serialNumber); + return ident >= 0 ? new UserHandle(ident) : null; + } + + /** + * Creates a user with the specified name and options. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * + * @param name the user's name + * @param flags flags that identify the type of user and other properties. + * @see UserInfo + * + * @return the UserInfo object for the created user, or null if the user could not be created. + * @hide + */ + public UserInfo createUser(String name, int flags) { + try { + return mService.createUser(name, flags); + } catch (RemoteException re) { + Log.w(TAG, "Could not create a user", re); + return null; + } + } + + /** + * Creates a guest user and configures it. + * @param context an application context + * @param name the name to set for the user + * @hide + */ + public UserInfo createGuest(Context context, String name) { + UserInfo guest = createUser(name, UserInfo.FLAG_GUEST); + if (guest != null) { + Settings.Secure.putStringForUser(context.getContentResolver(), + Settings.Secure.SKIP_FIRST_USE_HINTS, "1", guest.id); + try { + Bundle guestRestrictions = mService.getDefaultGuestRestrictions(); + guestRestrictions.putBoolean(DISALLOW_SMS, true); + guestRestrictions.putBoolean(DISALLOW_INSTALL_UNKNOWN_SOURCES, true); + mService.setUserRestrictions(guestRestrictions, guest.id); + } catch (RemoteException re) { + Log.w(TAG, "Could not update guest restrictions"); + } + } + return guest; + } + + /** + * Creates a secondary user with the specified name and options and configures it with default + * restrictions. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * + * @param name the user's name + * @param flags flags that identify the type of user and other properties. + * @see UserInfo + * + * @return the UserInfo object for the created user, or null if the user could not be created. + * @hide + */ + public UserInfo createSecondaryUser(String name, int flags) { + try { + UserInfo user = mService.createUser(name, flags); + if (user == null) { + return null; + } + Bundle userRestrictions = mService.getUserRestrictions(user.id); + addDefaultUserRestrictions(userRestrictions); + mService.setUserRestrictions(userRestrictions, user.id); + return user; + } catch (RemoteException re) { + Log.w(TAG, "Could not create a user", re); + return null; + } + } + + private static void addDefaultUserRestrictions(Bundle restrictions) { + restrictions.putBoolean(DISALLOW_OUTGOING_CALLS, true); + restrictions.putBoolean(DISALLOW_SMS, true); + } + + /** + * Creates a user with the specified name and options as a profile of another user. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * + * @param name the user's name + * @param flags flags that identify the type of user and other properties. + * @see UserInfo + * @param userHandle new user will be a profile of this use. + * + * @return the UserInfo object for the created user, or null if the user could not be created. + * @hide + */ + public UserInfo createProfileForUser(String name, int flags, int userHandle) { + try { + return mService.createProfileForUser(name, flags, userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Could not create a user", re); + return null; + } + } + + /** + * @hide + * Marks the guest user for deletion to allow a new guest to be created before deleting + * the current user who is a guest. + * @param userHandle + * @return + */ + public boolean markGuestForDeletion(int userHandle) { + try { + return mService.markGuestForDeletion(userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Could not mark guest for deletion", re); + return false; + } + } + + /** + * Sets the user as enabled, if such an user exists. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * Note that the default is true, it's only that managed profiles might not be enabled. + * + * @param userHandle the id of the profile to enable + * @hide + */ + public void setUserEnabled(int userHandle) { + try { + mService.setUserEnabled(userHandle); + } catch (RemoteException e) { + Log.w(TAG, "Could not enable the profile", e); + } + } + + /** + * Return the number of users currently created on the device. + */ + public int getUserCount() { + List users = getUsers(); + return users != null ? users.size() : 1; + } + + /** + * Returns information for all users on this device. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * @return the list of users that were created. + * @hide + */ + public List getUsers() { + try { + return mService.getUsers(false); + } catch (RemoteException re) { + Log.w(TAG, "Could not get user list", re); + return null; + } + } + + /** + * Checks whether it's possible to add more users. Caller must hold the MANAGE_USERS + * permission. + * + * @return true if more users can be added, false if limit has been reached. + * @hide + */ + public boolean canAddMoreUsers() { + final List users = getUsers(true); + final int totalUserCount = users.size(); + int aliveUserCount = 0; + for (int i = 0; i < totalUserCount; i++) { + UserInfo user = users.get(i); + if (!user.isGuest()) { + aliveUserCount++; + } + } + return aliveUserCount < getMaxSupportedUsers(); + } + + /** + * Returns list of the profiles of userHandle including + * userHandle itself. + * Note that this returns both enabled and not enabled profiles. See + * {@link #getUserProfiles()} if you need only the enabled ones. + * + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * @param userHandle profiles of this user will be returned. + * @return the list of profiles. + * @hide + */ + public List getProfiles(int userHandle) { + try { + return mService.getProfiles(userHandle, false /* enabledOnly */); + } catch (RemoteException re) { + Log.w(TAG, "Could not get user list", re); + return null; + } + } + + /** + * Returns a list of UserHandles for profiles associated with the user that the calling process + * is running on, including the user itself. + * + * @return A non-empty list of UserHandles associated with the calling user. + */ + public List getUserProfiles() { + ArrayList profiles = new ArrayList(); + List users = new ArrayList(); + try { + users = mService.getProfiles(UserHandle.myUserId(), true /* enabledOnly */); + } catch (RemoteException re) { + Log.w(TAG, "Could not get user list", re); + return null; + } + for (UserInfo info : users) { + UserHandle userHandle = new UserHandle(info.id); + profiles.add(userHandle); + } + return profiles; + } + + /** + * Returns the parent of the profile which this method is called from + * or null if called from a user that is not a profile. + * + * @hide + */ + public UserInfo getProfileParent(int userHandle) { + try { + return mService.getProfileParent(userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Could not get profile parent", re); + return null; + } + } + + /** + * If the target user is a managed profile of the calling user or the caller + * is itself a managed profile, then this returns a badged copy of the given + * icon to be able to distinguish it from the original icon. For badging an + * arbitrary drawable use {@link #getBadgedDrawableForUser( + * android.graphics.drawable.Drawable, UserHandle, android.graphics.Rect, int)}. + *

    + * If the original drawable is a BitmapDrawable and the backing bitmap is + * mutable as per {@link android.graphics.Bitmap#isMutable()}, the bading + * is performed in place and the original drawable is returned. + *

    + * + * @param icon The icon to badge. + * @param user The target user. + * @return A drawable that combines the original icon and a badge as + * determined by the system. + * @removed + */ + public Drawable getBadgedIconForUser(Drawable icon, UserHandle user) { + return mContext.getPackageManager().getUserBadgedIcon(icon, user); + } + + /** + * If the target user is a managed profile of the calling user or the caller + * is itself a managed profile, then this returns a badged copy of the given + * drawable allowing the user to distinguish it from the original drawable. + * The caller can specify the location in the bounds of the drawable to be + * badged where the badge should be applied as well as the density of the + * badge to be used. + *

    + * If the original drawable is a BitmapDrawable and the backing bitmap is + * mutable as per {@link android.graphics.Bitmap#isMutable()}, the bading + * is performed in place and the original drawable is returned. + *

    + * + * @param badgedDrawable The drawable to badge. + * @param user The target user. + * @param badgeLocation Where in the bounds of the badged drawable to place + * the badge. If not provided, the badge is applied on top of the entire + * drawable being badged. + * @param badgeDensity The optional desired density for the badge as per + * {@link android.util.DisplayMetrics#densityDpi}. If not provided, + * the density of the display is used. + * @return A drawable that combines the original drawable and a badge as + * determined by the system. + * @removed + */ + public Drawable getBadgedDrawableForUser(Drawable badgedDrawable, UserHandle user, + Rect badgeLocation, int badgeDensity) { + return mContext.getPackageManager().getUserBadgedDrawableForDensity(badgedDrawable, user, + badgeLocation, badgeDensity); + } + + /** + * If the target user is a managed profile of the calling user or the caller + * is itself a managed profile, then this returns a copy of the label with + * badging for accessibility services like talkback. E.g. passing in "Email" + * and it might return "Work Email" for Email in the work profile. + * + * @param label The label to change. + * @param user The target user. + * @return A label that combines the original label and a badge as + * determined by the system. + * @removed + */ + public CharSequence getBadgedLabelForUser(CharSequence label, UserHandle user) { + return mContext.getPackageManager().getUserBadgedLabel(label, user); + } + + /** + * Returns information for all users on this device. Requires + * {@link android.Manifest.permission#MANAGE_USERS} permission. + * + * @param excludeDying specify if the list should exclude users being + * removed. + * @return the list of users that were created. + * @hide + */ + public List getUsers(boolean excludeDying) { + try { + return mService.getUsers(excludeDying); + } catch (RemoteException re) { + Log.w(TAG, "Could not get user list", re); + return null; + } + } + + /** + * Removes a user and all associated data. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * @param userHandle the integer handle of the user, where 0 is the primary user. + * @hide + */ + public boolean removeUser(int userHandle) { + try { + return mService.removeUser(userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Could not remove user ", re); + return false; + } + } + + /** + * Updates the user's name. + * Requires {@link android.Manifest.permission#MANAGE_USERS} permission. + * + * @param userHandle the user's integer handle + * @param name the new name for the user + * @hide + */ + public void setUserName(int userHandle, String name) { + try { + mService.setUserName(userHandle, name); + } catch (RemoteException re) { + Log.w(TAG, "Could not set the user name ", re); + } + } + + /** + * Sets the user's photo. + * @param userHandle the user for whom to change the photo. + * @param icon the bitmap to set as the photo. + * @hide + */ + public void setUserIcon(int userHandle, Bitmap icon) { + try { + mService.setUserIcon(userHandle, icon); + } catch (RemoteException re) { + Log.w(TAG, "Could not set the user icon ", re); + } + } + + /** + * Returns a file descriptor for the user's photo. PNG data can be read from this file. + * @param userHandle the user whose photo we want to read. + * @return a {@link Bitmap} of the user's photo, or null if there's no photo. + * @see com.android.internal.util.UserIcons#getDefaultUserIcon for a default. + * @hide + */ + public Bitmap getUserIcon(int userHandle) { + try { + return mService.getUserIcon(userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Could not get the user icon ", re); + return null; + } + } + + /** + * Returns the maximum number of users that can be created on this device. A return value + * of 1 means that it is a single user device. + * @hide + * @return a value greater than or equal to 1 + */ + public static int getMaxSupportedUsers() { + // Don't allow multiple users on certain builds + if (android.os.Build.ID.startsWith("JVP")) return 1; + // Svelte devices don't get multi-user. + if (ActivityManager.isLowRamDeviceStatic()) return 1; + return SystemProperties.getInt("fw.max_users", + Resources.getSystem().getInteger(R.integer.config_multiuserMaximumUsers)); + } + + /** + * Returns true if the user switcher should be shown, this will be if there + * are multiple users that aren't managed profiles. + * @hide + * @return true if user switcher should be shown. + */ + public boolean isUserSwitcherEnabled() { + List users = getUsers(true); + if (users == null) { + return false; + } + int switchableUserCount = 0; + for (UserInfo user : users) { + if (user.supportsSwitchTo()) { + ++switchableUserCount; + } + } + final boolean guestEnabled = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.GUEST_USER_ENABLED, 0) == 1; + return switchableUserCount > 1 || guestEnabled; + } + + /** + * Returns a serial number on this device for a given userHandle. User handles can be recycled + * when deleting and creating users, but serial numbers are not reused until the device is wiped. + * @param userHandle + * @return a serial number associated with that user, or -1 if the userHandle is not valid. + * @hide + */ + public int getUserSerialNumber(int userHandle) { + try { + return mService.getUserSerialNumber(userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Could not get serial number for user " + userHandle); + } + return -1; + } + + /** + * Returns a userHandle on this device for a given user serial number. User handles can be + * recycled when deleting and creating users, but serial numbers are not reused until the device + * is wiped. + * @param userSerialNumber + * @return the userHandle associated with that user serial number, or -1 if the serial number + * is not valid. + * @hide + */ + public int getUserHandle(int userSerialNumber) { + try { + return mService.getUserHandle(userSerialNumber); + } catch (RemoteException re) { + Log.w(TAG, "Could not get userHandle for user " + userSerialNumber); + } + return -1; + } + + /** + * Returns a Bundle containing any saved application restrictions for this user, for the + * given package name. Only an application with this package name can call this method. + * @param packageName the package name of the calling application + * @return a Bundle with the restrictions as key/value pairs, or null if there are no + * saved restrictions. The values can be of type Boolean, String or String[], depending + * on the restriction type, as defined by the application. + */ + public Bundle getApplicationRestrictions(String packageName) { + try { + return mService.getApplicationRestrictions(packageName); + } catch (RemoteException re) { + Log.w(TAG, "Could not get application restrictions for package " + packageName); + } + return null; + } + + /** + * @hide + */ + public Bundle getApplicationRestrictions(String packageName, UserHandle user) { + try { + return mService.getApplicationRestrictionsForUser(packageName, user.getIdentifier()); + } catch (RemoteException re) { + Log.w(TAG, "Could not get application restrictions for user " + user.getIdentifier()); + } + return null; + } + + /** + * @hide + */ + public void setApplicationRestrictions(String packageName, Bundle restrictions, + UserHandle user) { + try { + mService.setApplicationRestrictions(packageName, restrictions, user.getIdentifier()); + } catch (RemoteException re) { + Log.w(TAG, "Could not set application restrictions for user " + user.getIdentifier()); + } + } + + /** + * Sets a new challenge PIN for restrictions. This is only for use by pre-installed + * apps and requires the MANAGE_USERS permission. + * @param newPin the PIN to use for challenge dialogs. + * @return Returns true if the challenge PIN was set successfully. + */ + public boolean setRestrictionsChallenge(String newPin) { + try { + return mService.setRestrictionsChallenge(newPin); + } catch (RemoteException re) { + Log.w(TAG, "Could not change restrictions pin"); + } + return false; + } + + /** + * @hide + * @param pin The PIN to verify, or null to get the number of milliseconds to wait for before + * allowing the user to enter the PIN. + * @return Returns a positive number (including zero) for how many milliseconds before + * you can accept another PIN, when the input is null or the input doesn't match the saved PIN. + * Returns {@link #PIN_VERIFICATION_SUCCESS} if the input matches the saved PIN. Returns + * {@link #PIN_VERIFICATION_FAILED_NOT_SET} if there is no PIN set. + */ + public int checkRestrictionsChallenge(String pin) { + try { + return mService.checkRestrictionsChallenge(pin); + } catch (RemoteException re) { + Log.w(TAG, "Could not check restrictions pin"); + } + return PIN_VERIFICATION_FAILED_INCORRECT; + } + + /** + * @hide + * Checks whether the user has restrictions that are PIN-protected. An application that + * participates in restrictions can check if the owner has requested a PIN challenge for + * any restricted operations. If there is a PIN in effect, the application should launch + * the PIN challenge activity {@link android.content.Intent#ACTION_RESTRICTIONS_CHALLENGE}. + * @see android.content.Intent#ACTION_RESTRICTIONS_CHALLENGE + * @return whether a restrictions PIN is in effect. + */ + public boolean hasRestrictionsChallenge() { + try { + return mService.hasRestrictionsChallenge(); + } catch (RemoteException re) { + Log.w(TAG, "Could not change restrictions pin"); + } + return false; + } + + /** @hide */ + public void removeRestrictions() { + try { + mService.removeRestrictions(); + } catch (RemoteException re) { + Log.w(TAG, "Could not change restrictions pin"); + } + } + + /** + * @hide + * Set restrictions that should apply to any future guest user that's created. + */ + public void setDefaultGuestRestrictions(Bundle restrictions) { + try { + mService.setDefaultGuestRestrictions(restrictions); + } catch (RemoteException re) { + Log.w(TAG, "Could not set guest restrictions"); + } + } + + /** + * @hide + * Gets the default guest restrictions. + */ + public Bundle getDefaultGuestRestrictions() { + try { + return mService.getDefaultGuestRestrictions(); + } catch (RemoteException re) { + Log.w(TAG, "Could not set guest restrictions"); + } + return new Bundle(); + } +} diff --git a/src/main/java/android/os/Vibrator.java b/src/main/java/android/os/Vibrator.java new file mode 100644 index 0000000..f9b7666 --- /dev/null +++ b/src/main/java/android/os/Vibrator.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2006 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 android.os; + +import android.app.ActivityThread; +import android.content.Context; +import android.media.AudioAttributes; + +/** + * Class that operates the vibrator on the device. + *

    + * If your process exits, any vibration you started will stop. + *

    + * + * To obtain an instance of the system vibrator, call + * {@link Context#getSystemService} with {@link Context#VIBRATOR_SERVICE} as the argument. + */ +public abstract class Vibrator { + + private final String mPackageName; + + /** + * @hide to prevent subclassing from outside of the framework + */ + public Vibrator() { + mPackageName = ActivityThread.currentPackageName(); + } + + /** + * @hide to prevent subclassing from outside of the framework + */ + protected Vibrator(Context context) { + mPackageName = context.getOpPackageName(); + } + + /** + * Check whether the hardware has a vibrator. + * + * @return True if the hardware has a vibrator, else false. + */ + public abstract boolean hasVibrator(); + + /** + * Vibrate constantly for the specified period of time. + *

    This method requires the caller to hold the permission + * {@link android.Manifest.permission#VIBRATE}. + * + * @param milliseconds The number of milliseconds to vibrate. + */ + public void vibrate(long milliseconds) { + vibrate(milliseconds, null); + } + + /** + * Vibrate constantly for the specified period of time. + *

    This method requires the caller to hold the permission + * {@link android.Manifest.permission#VIBRATE}. + * + * @param milliseconds The number of milliseconds to vibrate. + * @param attributes {@link AudioAttributes} corresponding to the vibration. For example, + * specify {@link AudioAttributes#USAGE_ALARM} for alarm vibrations or + * {@link AudioAttributes#USAGE_NOTIFICATION_RINGTONE} for + * vibrations associated with incoming calls. + */ + public void vibrate(long milliseconds, AudioAttributes attributes) { + vibrate(Process.myUid(), mPackageName, milliseconds, attributes); + } + + /** + * Vibrate with a given pattern. + * + *

    + * Pass in an array of ints that are the durations for which to turn on or off + * the vibrator in milliseconds. The first value indicates the number of milliseconds + * to wait before turning the vibrator on. The next value indicates the number of milliseconds + * for which to keep the vibrator on before turning it off. Subsequent values alternate + * between durations in milliseconds to turn the vibrator off or to turn the vibrator on. + *

    + * To cause the pattern to repeat, pass the index into the pattern array at which + * to start the repeat, or -1 to disable repeating. + *

    + *

    This method requires the caller to hold the permission + * {@link android.Manifest.permission#VIBRATE}. + * + * @param pattern an array of longs of times for which to turn the vibrator on or off. + * @param repeat the index into pattern at which to repeat, or -1 if + * you don't want to repeat. + */ + public void vibrate(long[] pattern, int repeat) { + vibrate(pattern, repeat, null); + } + + /** + * Vibrate with a given pattern. + * + *

    + * Pass in an array of ints that are the durations for which to turn on or off + * the vibrator in milliseconds. The first value indicates the number of milliseconds + * to wait before turning the vibrator on. The next value indicates the number of milliseconds + * for which to keep the vibrator on before turning it off. Subsequent values alternate + * between durations in milliseconds to turn the vibrator off or to turn the vibrator on. + *

    + * To cause the pattern to repeat, pass the index into the pattern array at which + * to start the repeat, or -1 to disable repeating. + *

    + *

    This method requires the caller to hold the permission + * {@link android.Manifest.permission#VIBRATE}. + * + * @param pattern an array of longs of times for which to turn the vibrator on or off. + * @param repeat the index into pattern at which to repeat, or -1 if + * you don't want to repeat. + * @param attributes {@link AudioAttributes} corresponding to the vibration. For example, + * specify {@link AudioAttributes#USAGE_ALARM} for alarm vibrations or + * {@link AudioAttributes#USAGE_NOTIFICATION_RINGTONE} for + * vibrations associated with incoming calls. + */ + public void vibrate(long[] pattern, int repeat, AudioAttributes attributes) { + vibrate(Process.myUid(), mPackageName, pattern, repeat, attributes); + } + + /** + * @hide + * Like {@link #vibrate(long, AudioAttributes)}, but allowing the caller to specify that + * the vibration is owned by someone else. + */ + public abstract void vibrate(int uid, String opPkg, long milliseconds, + AudioAttributes attributes); + + /** + * @hide + * Like {@link #vibrate(long[], int, AudioAttributes)}, but allowing the caller to specify that + * the vibration is owned by someone else. + */ + public abstract void vibrate(int uid, String opPkg, long[] pattern, int repeat, + AudioAttributes attributes); + + /** + * Turn the vibrator off. + *

    This method requires the caller to hold the permission + * {@link android.Manifest.permission#VIBRATE}. + */ + public abstract void cancel(); +} diff --git a/src/main/java/android/os/WorkSource.java b/src/main/java/android/os/WorkSource.java new file mode 100644 index 0000000..f8da87a --- /dev/null +++ b/src/main/java/android/os/WorkSource.java @@ -0,0 +1,704 @@ +package android.os; + +import android.util.Log; + +import java.util.Arrays; + +/** + * Describes the source of some work that may be done by someone else. + * Currently the public representation of what a work source is is not + * defined; this is an opaque container. + */ +public class WorkSource implements Parcelable { + static final String TAG = "WorkSource"; + static final boolean DEBUG = false; + + int mNum; + int[] mUids; + String[] mNames; + + /** + * Internal statics to avoid object allocations in some operations. + * The WorkSource object itself is not thread safe, but we need to + * hold sTmpWorkSource lock while working with these statics. + */ + static final WorkSource sTmpWorkSource = new WorkSource(0); + /** + * For returning newbie work from a modification operation. + */ + static WorkSource sNewbWork; + /** + * For returning gone work form a modification operation. + */ + static WorkSource sGoneWork; + + /** + * Create an empty work source. + */ + public WorkSource() { + mNum = 0; + } + + /** + * Create a new WorkSource that is a copy of an existing one. + * If orig is null, an empty WorkSource is created. + */ + public WorkSource(WorkSource orig) { + if (orig == null) { + mNum = 0; + return; + } + mNum = orig.mNum; + if (orig.mUids != null) { + mUids = orig.mUids.clone(); + mNames = orig.mNames != null ? orig.mNames.clone() : null; + } else { + mUids = null; + mNames = null; + } + } + + /** @hide */ + public WorkSource(int uid) { + mNum = 1; + mUids = new int[] { uid, 0 }; + mNames = null; + } + + /** @hide */ + public WorkSource(int uid, String name) { + if (name == null) { + throw new NullPointerException("Name can't be null"); + } + mNum = 1; + mUids = new int[] { uid, 0 }; + mNames = new String[] { name, null }; + } + + WorkSource(Parcel in) { + mNum = in.readInt(); + mUids = in.createIntArray(); + mNames = in.createStringArray(); + } + + /** @hide */ + public int size() { + return mNum; + } + + /** @hide */ + public int get(int index) { + return mUids[index]; + } + + /** @hide */ + public String getName(int index) { + return mNames != null ? mNames[index] : null; + } + + /** + * Clear names from this WorkSource. Uids are left intact. + * + *

    Useful when combining with another WorkSource that doesn't have names. + * @hide + */ + public void clearNames() { + if (mNames != null) { + mNames = null; + // Clear out any duplicate uids now that we don't have names to disambiguate them. + int destIndex = 1; + int newNum = mNum; + for (int sourceIndex = 1; sourceIndex < mNum; sourceIndex++) { + if (mUids[sourceIndex] == mUids[sourceIndex - 1]) { + newNum--; + } else { + mUids[destIndex] = mUids[sourceIndex]; + destIndex++; + } + } + mNum = newNum; + } + } + + /** + * Clear this WorkSource to be empty. + */ + public void clear() { + mNum = 0; + } + + @Override + public boolean equals(Object o) { + return o instanceof WorkSource && !diff((WorkSource)o); + } + + @Override + public int hashCode() { + int result = 0; + for (int i = 0; i < mNum; i++) { + result = ((result << 4) | (result >>> 28)) ^ mUids[i]; + } + if (mNames != null) { + for (int i = 0; i < mNum; i++) { + result = ((result << 4) | (result >>> 28)) ^ mNames[i].hashCode(); + } + } + return result; + } + + /** + * Compare this WorkSource with another. + * @param other The WorkSource to compare against. + * @return If there is a difference, true is returned. + */ + public boolean diff(WorkSource other) { + int N = mNum; + if (N != other.mNum) { + return true; + } + final int[] uids1 = mUids; + final int[] uids2 = other.mUids; + final String[] names1 = mNames; + final String[] names2 = other.mNames; + for (int i=0; iother is null, the current work source + * will be made empty. + */ + public void set(WorkSource other) { + if (other == null) { + mNum = 0; + return; + } + mNum = other.mNum; + if (other.mUids != null) { + if (mUids != null && mUids.length >= mNum) { + System.arraycopy(other.mUids, 0, mUids, 0, mNum); + } else { + mUids = other.mUids.clone(); + } + if (other.mNames != null) { + if (mNames != null && mNames.length >= mNum) { + System.arraycopy(other.mNames, 0, mNames, 0, mNum); + } else { + mNames = other.mNames.clone(); + } + } else { + mNames = null; + } + } else { + mUids = null; + mNames = null; + } + } + + /** @hide */ + public void set(int uid) { + mNum = 1; + if (mUids == null) mUids = new int[2]; + mUids[0] = uid; + mNames = null; + } + + /** @hide */ + public void set(int uid, String name) { + if (name == null) { + throw new NullPointerException("Name can't be null"); + } + mNum = 1; + if (mUids == null) { + mUids = new int[2]; + mNames = new String[2]; + } + mUids[0] = uid; + mNames[0] = name; + } + + /** @hide */ + public WorkSource[] setReturningDiffs(WorkSource other) { + synchronized (sTmpWorkSource) { + sNewbWork = null; + sGoneWork = null; + updateLocked(other, true, true); + if (sNewbWork != null || sGoneWork != null) { + WorkSource[] diffs = new WorkSource[2]; + diffs[0] = sNewbWork; + diffs[1] = sGoneWork; + return diffs; + } + return null; + } + } + + /** + * Merge the contents of other WorkSource in to this one. + * + * @param other The other WorkSource whose contents are to be merged. + * @return Returns true if any new sources were added. + */ + public boolean add(WorkSource other) { + synchronized (sTmpWorkSource) { + return updateLocked(other, false, false); + } + } + + /** @hide */ + public WorkSource addReturningNewbs(WorkSource other) { + synchronized (sTmpWorkSource) { + sNewbWork = null; + updateLocked(other, false, true); + return sNewbWork; + } + } + + /** @hide */ + public boolean add(int uid) { + if (mNum <= 0) { + mNames = null; + insert(0, uid); + return true; + } + if (mNames != null) { + throw new IllegalArgumentException("Adding without name to named " + this); + } + int i = Arrays.binarySearch(mUids, 0, mNum, uid); + if (DEBUG) Log.d(TAG, "Adding uid " + uid + " to " + this + ": binsearch res = " + i); + if (i >= 0) { + return false; + } + insert(-i-1, uid); + return true; + } + + /** @hide */ + public boolean add(int uid, String name) { + if (mNum <= 0) { + insert(0, uid, name); + return true; + } + if (mNames == null) { + throw new IllegalArgumentException("Adding name to unnamed " + this); + } + int i; + for (i=0; i uid) { + break; + } + if (mUids[i] == uid) { + int diff = mNames[i].compareTo(name); + if (diff > 0) { + break; + } + if (diff == 0) { + return false; + } + } + } + insert(i, uid, name); + return true; + } + + /** @hide */ + public WorkSource addReturningNewbs(int uid) { + synchronized (sTmpWorkSource) { + sNewbWork = null; + sTmpWorkSource.mUids[0] = uid; + updateLocked(sTmpWorkSource, false, true); + return sNewbWork; + } + } + + public boolean remove(WorkSource other) { + if (mNum <= 0 || other.mNum <= 0) { + return false; + } + if (mNames == null && other.mNames == null) { + return removeUids(other); + } else { + if (mNames == null) { + throw new IllegalArgumentException("Other " + other + " has names, but target " + + this + " does not"); + } + if (other.mNames == null) { + throw new IllegalArgumentException("Target " + this + " has names, but other " + + other + " does not"); + } + return removeUidsAndNames(other); + } + } + + /** @hide */ + public WorkSource stripNames() { + if (mNum <= 0) { + return new WorkSource(); + } + WorkSource result = new WorkSource(); + int lastUid = -1; + for (int i=0; i uids1[i1]) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip i1"); + i1++; + } else { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip i2"); + i2++; + } + } + + mNum = N1; + + return changed; + } + + private boolean removeUidsAndNames(WorkSource other) { + int N1 = mNum; + final int[] uids1 = mUids; + final String[] names1 = mNames; + final int N2 = other.mNum; + final int[] uids2 = other.mUids; + final String[] names2 = other.mNames; + boolean changed = false; + int i1 = 0, i2 = 0; + if (DEBUG) Log.d(TAG, "Remove " + other + " from " + this); + while (i1 < N1 && i2 < N2) { + if (DEBUG) Log.d(TAG, "Step: target @ " + i1 + " of " + N1 + ", other @ " + i2 + + " of " + N2 + ": " + uids1[i1] + " " + names1[i1]); + if (uids2[i2] == uids1[i1] && names2[i2].equals(names1[i1])) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + + ": remove " + uids1[i1] + " " + names1[i1]); + N1--; + changed = true; + if (i1 < N1) { + System.arraycopy(uids1, i1+1, uids1, i1, N1-i1); + System.arraycopy(names1, i1+1, names1, i1, N1-i1); + } + i2++; + } else if (uids2[i2] > uids1[i1] + || (uids2[i2] == uids1[i1] && names2[i2].compareTo(names1[i1]) > 0)) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip i1"); + i1++; + } else { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip i2"); + i2++; + } + } + + mNum = N1; + + return changed; + } + + private boolean updateLocked(WorkSource other, boolean set, boolean returnNewbs) { + if (mNames == null && other.mNames == null) { + return updateUidsLocked(other, set, returnNewbs); + } else { + if (mNum > 0 && mNames == null) { + throw new IllegalArgumentException("Other " + other + " has names, but target " + + this + " does not"); + } + if (other.mNum > 0 && other.mNames == null) { + throw new IllegalArgumentException("Target " + this + " has names, but other " + + other + " does not"); + } + return updateUidsAndNamesLocked(other, set, returnNewbs); + } + } + + private static WorkSource addWork(WorkSource cur, int newUid) { + if (cur == null) { + return new WorkSource(newUid); + } + cur.insert(cur.mNum, newUid); + return cur; + } + + private boolean updateUidsLocked(WorkSource other, boolean set, boolean returnNewbs) { + int N1 = mNum; + int[] uids1 = mUids; + final int N2 = other.mNum; + final int[] uids2 = other.mUids; + boolean changed = false; + int i1 = 0, i2 = 0; + if (DEBUG) Log.d(TAG, "Update " + this + " with " + other + " set=" + set + + " returnNewbs=" + returnNewbs); + while (i1 < N1 || i2 < N2) { + if (DEBUG) Log.d(TAG, "Step: target @ " + i1 + " of " + N1 + ", other @ " + i2 + + " of " + N2); + if (i1 >= N1 || (i2 < N2 && uids2[i2] < uids1[i1])) { + // Need to insert a new uid. + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + + ": insert " + uids2[i2]); + changed = true; + if (uids1 == null) { + uids1 = new int[4]; + uids1[0] = uids2[i2]; + } else if (N1 >= uids1.length) { + int[] newuids = new int[(uids1.length*3)/2]; + if (i1 > 0) System.arraycopy(uids1, 0, newuids, 0, i1); + if (i1 < N1) System.arraycopy(uids1, i1, newuids, i1+1, N1-i1); + uids1 = newuids; + uids1[i1] = uids2[i2]; + } else { + if (i1 < N1) System.arraycopy(uids1, i1, uids1, i1+1, N1-i1); + uids1[i1] = uids2[i2]; + } + if (returnNewbs) { + sNewbWork = addWork(sNewbWork, uids2[i2]); + } + N1++; + i1++; + i2++; + } else { + if (!set) { + // Skip uids that already exist or are not in 'other'. + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip"); + if (i2 < N2 && uids2[i2] == uids1[i1]) { + i2++; + } + i1++; + } else { + // Remove any uids that don't exist in 'other'. + int start = i1; + while (i1 < N1 && (i2 >= N2 || uids2[i2] > uids1[i1])) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + ": remove " + uids1[i1]); + sGoneWork = addWork(sGoneWork, uids1[i1]); + i1++; + } + if (start < i1) { + System.arraycopy(uids1, i1, uids1, start, N1-i1); + N1 -= i1-start; + i1 = start; + } + // If there is a matching uid, skip it. + if (i1 < N1 && i2 < N2 && uids2[i2] == uids1[i1]) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + N1 + ": skip"); + i1++; + i2++; + } + } + } + } + + mNum = N1; + mUids = uids1; + + return changed; + } + + /** + * Returns 0 if equal, negative if 'this' is before 'other', positive if 'this' is after 'other'. + */ + private int compare(WorkSource other, int i1, int i2) { + final int diff = mUids[i1] - other.mUids[i2]; + if (diff != 0) { + return diff; + } + return mNames[i1].compareTo(other.mNames[i2]); + } + + private static WorkSource addWork(WorkSource cur, int newUid, String newName) { + if (cur == null) { + return new WorkSource(newUid, newName); + } + cur.insert(cur.mNum, newUid, newName); + return cur; + } + + private boolean updateUidsAndNamesLocked(WorkSource other, boolean set, boolean returnNewbs) { + final int N2 = other.mNum; + final int[] uids2 = other.mUids; + String[] names2 = other.mNames; + boolean changed = false; + int i1 = 0, i2 = 0; + if (DEBUG) Log.d(TAG, "Update " + this + " with " + other + " set=" + set + + " returnNewbs=" + returnNewbs); + while (i1 < mNum || i2 < N2) { + if (DEBUG) Log.d(TAG, "Step: target @ " + i1 + " of " + mNum + ", other @ " + i2 + + " of " + N2); + int diff = -1; + if (i1 >= mNum || (i2 < N2 && (diff=compare(other, i1, i2)) > 0)) { + // Need to insert a new uid. + changed = true; + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + mNum + + ": insert " + uids2[i2] + " " + names2[i2]); + insert(i1, uids2[i2], names2[i2]); + if (returnNewbs) { + sNewbWork = addWork(sNewbWork, uids2[i2], names2[i2]); + } + i1++; + i2++; + } else { + if (!set) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + mNum + ": skip"); + if (i2 < N2 && diff == 0) { + i2++; + } + i1++; + } else { + // Remove any uids that don't exist in 'other'. + int start = i1; + while (diff < 0) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + ": remove " + mUids[i1] + + " " + mNames[i1]); + sGoneWork = addWork(sGoneWork, mUids[i1], mNames[i1]); + i1++; + if (i1 >= mNum) { + break; + } + diff = i2 < N2 ? compare(other, i1, i2) : -1; + } + if (start < i1) { + System.arraycopy(mUids, i1, mUids, start, mNum-i1); + System.arraycopy(mNames, i1, mNames, start, mNum-i1); + mNum -= i1-start; + i1 = start; + } + // If there is a matching uid, skip it. + if (i1 < mNum && diff == 0) { + if (DEBUG) Log.d(TAG, "i1=" + i1 + " i2=" + i2 + " N1=" + mNum + ": skip"); + i1++; + i2++; + } + } + } + } + + return changed; + } + + private void insert(int index, int uid) { + if (DEBUG) Log.d(TAG, "Insert in " + this + " @ " + index + " uid " + uid); + if (mUids == null) { + mUids = new int[4]; + mUids[0] = uid; + mNum = 1; + } else if (mNum >= mUids.length) { + int[] newuids = new int[(mNum*3)/2]; + if (index > 0) { + System.arraycopy(mUids, 0, newuids, 0, index); + } + if (index < mNum) { + System.arraycopy(mUids, index, newuids, index+1, mNum-index); + } + mUids = newuids; + mUids[index] = uid; + mNum++; + } else { + if (index < mNum) { + System.arraycopy(mUids, index, mUids, index+1, mNum-index); + } + mUids[index] = uid; + mNum++; + } + } + + private void insert(int index, int uid, String name) { + if (mUids == null) { + mUids = new int[4]; + mUids[0] = uid; + mNames = new String[4]; + mNames[0] = name; + mNum = 1; + } else if (mNum >= mUids.length) { + int[] newuids = new int[(mNum*3)/2]; + String[] newnames = new String[(mNum*3)/2]; + if (index > 0) { + System.arraycopy(mUids, 0, newuids, 0, index); + System.arraycopy(mNames, 0, newnames, 0, index); + } + if (index < mNum) { + System.arraycopy(mUids, index, newuids, index+1, mNum-index); + System.arraycopy(mNames, index, newnames, index+1, mNum-index); + } + mUids = newuids; + mNames = newnames; + mUids[index] = uid; + mNames[index] = name; + mNum++; + } else { + if (index < mNum) { + System.arraycopy(mUids, index, mUids, index+1, mNum-index); + System.arraycopy(mNames, index, mNames, index+1, mNum-index); + } + mUids[index] = uid; + mNames[index] = name; + mNum++; + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mNum); + dest.writeIntArray(mUids); + dest.writeStringArray(mNames); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("WorkSource{"); + for (int i = 0; i < mNum; i++) { + if (i != 0) { + result.append(", "); + } + result.append(mUids[i]); + if (mNames != null) { + result.append(" "); + result.append(mNames[i]); + } + } + result.append("}"); + return result.toString(); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public WorkSource createFromParcel(Parcel in) { + return new WorkSource(in); + } + public WorkSource[] newArray(int size) { + return new WorkSource[size]; + } + }; +} diff --git a/src/main/java/android/os/storage/AsecTests.java b/src/main/java/android/os/storage/AsecTests.java new file mode 100644 index 0000000..4f724fe --- /dev/null +++ b/src/main/java/android/os/storage/AsecTests.java @@ -0,0 +1,756 @@ +/* + * Copyright (C) 2006 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 android.os.storage; + +import android.content.Context; +import android.os.Environment; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.test.AndroidTestCase; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; + +public class AsecTests extends AndroidTestCase { + private static final String SECURE_CONTAINER_PREFIX = "com.android.unittests.AsecTests."; + private static final boolean localLOGV = true; + public static final String TAG="AsecTests"; + + private static final String FS_FAT = "fat"; + private static final String FS_EXT4 = "ext4"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (localLOGV) Log.i(TAG, "Cleaning out old test containers"); + cleanupContainers(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + if (localLOGV) Log.i(TAG, "Cleaning out old test containers"); + cleanupContainers(); + } + + private void cleanupContainers() throws RemoteException { + IMountService ms = getMs(); + String[] containers = ms.getSecureContainerList(); + + for (int i = 0; i < containers.length; i++) { + if (containers[i].startsWith(SECURE_CONTAINER_PREFIX)) { + if (localLOGV) + Log.i(TAG, "Cleaning: " + containers[i]); + ms.destroySecureContainer(containers[i], true); + } + } + } + + private boolean containerExists(String localId) throws RemoteException { + IMountService ms = getMs(); + String[] containers = ms.getSecureContainerList(); + String fullId = SECURE_CONTAINER_PREFIX + localId; + + for (int i = 0; i < containers.length; i++) { + if (containers[i].equals(fullId)) { + return true; + } + } + return false; + } + + private int createContainer(String localId, int size, String key, String filesystem, + boolean isExternal) throws Exception { + assertTrue("Media should be mounted", isMediaMounted()); + String fullId = SECURE_CONTAINER_PREFIX + localId; + + IMountService ms = getMs(); + return ms.createSecureContainer(fullId, size, filesystem, key, android.os.Process.myUid(), + isExternal); + } + + private int mountContainer(String localId, String key) throws Exception { + assertTrue("Media should be mounted", isMediaMounted()); + String fullId = SECURE_CONTAINER_PREFIX + localId; + + IMountService ms = getMs(); + return ms.mountSecureContainer(fullId, key, android.os.Process.myUid(), true); + } + + private int renameContainer(String localId1, String localId2) throws Exception { + assertTrue("Media should be mounted", isMediaMounted()); + String fullId1 = SECURE_CONTAINER_PREFIX + localId1; + String fullId2 = SECURE_CONTAINER_PREFIX + localId2; + + IMountService ms = getMs(); + return ms.renameSecureContainer(fullId1, fullId2); + } + + private int unmountContainer(String localId, boolean force) throws Exception { + assertTrue("Media should be mounted", isMediaMounted()); + String fullId = SECURE_CONTAINER_PREFIX + localId; + + IMountService ms = getMs(); + return ms.unmountSecureContainer(fullId, force); + } + + private int destroyContainer(String localId, boolean force) throws Exception { + assertTrue("Media should be mounted", isMediaMounted()); + String fullId = SECURE_CONTAINER_PREFIX + localId; + + IMountService ms = getMs(); + return ms.destroySecureContainer(fullId, force); + } + + private boolean isContainerMounted(String localId) throws Exception { + assertTrue("Media should be mounted", isMediaMounted()); + String fullId = SECURE_CONTAINER_PREFIX + localId; + + IMountService ms = getMs(); + return ms.isSecureContainerMounted(fullId); + } + + private IMountService getMs() { + IBinder service = ServiceManager.getService("mount"); + if (service != null) { + return IMountService.Stub.asInterface(service); + } else { + Log.e(TAG, "Can't get mount service"); + } + return null; + } + + private boolean isMediaMounted() throws Exception { + String mPath = Environment.getExternalStorageDirectory().toString(); + String state = getMs().getVolumeState(mPath); + return Environment.MEDIA_MOUNTED.equals(state); + } + + + /* + * CREATE + */ + + public void test_Fat_External_Create_Success() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateContainer", 4, "none", FS_FAT, true)); + assertTrue(containerExists("testCreateContainer")); + } + + public void test_Ext4_External_Create_Success() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateContainer", 4, "none", FS_EXT4, true)); + assertTrue(containerExists("testCreateContainer")); + } + + public void test_Fat_Internal_Create_Success() throws Exception { + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateContainer", 4, "none", FS_FAT, false)); + assertTrue(containerExists("testCreateContainer")); + } + + public void test_Ext4_Internal_Create_Success() throws Exception { + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateContainer", 4, "none", FS_EXT4, false)); + assertTrue(containerExists("testCreateContainer")); + } + + + /* + * CREATE MIN SIZE + */ + + public void test_Fat_External_CreateMinSize_Success() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateContainer", 1, "none", FS_FAT, true)); + assertTrue(containerExists("testCreateContainer")); + } + + public void test_Ext4_External_CreateMinSize_Success() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateContainer", 1, "none", FS_EXT4, true)); + assertTrue(containerExists("testCreateContainer")); + } + + public void test_Fat_Internal_CreateMinSize_Success() throws Exception { + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateContainer", 1, "none", FS_FAT, false)); + assertTrue(containerExists("testCreateContainer")); + } + + public void test_Ext4_Internal_CreateMinSize_Success() throws Exception { + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateContainer", 1, "none", FS_EXT4, false)); + assertTrue(containerExists("testCreateContainer")); + } + + + /* + * CREATE ZERO SIZE - FAIL CASE + */ + + public void test_Fat_External_CreateZeroSize_Failure() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationFailedInternalError, + createContainer("testCreateZeroContainer", 0, "none", FS_FAT, true)); + } + + public void test_Ext4_External_CreateZeroSize_Failure() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationFailedInternalError, + createContainer("testCreateZeroContainer", 0, "none", FS_EXT4, true)); + } + + public void test_Fat_Internal_CreateZeroSize_Failure() throws Exception { + assertEquals(StorageResultCode.OperationFailedInternalError, + createContainer("testCreateZeroContainer", 0, "none", FS_FAT, false)); + } + + public void test_Ext4_Internal_CreateZeroSize_Failure() throws Exception { + assertEquals(StorageResultCode.OperationFailedInternalError, + createContainer("testCreateZeroContainer", 0, "none", FS_EXT4, false)); + } + + + /* + * CREATE DUPLICATE - FAIL CASE + */ + + public void test_Fat_External_CreateDuplicate_Failure() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateDupContainer", 4, "none", FS_FAT, true)); + + assertEquals(StorageResultCode.OperationFailedInternalError, + createContainer("testCreateDupContainer", 4, "none", FS_FAT, true)); + } + + public void test_Ext4_External_CreateDuplicate_Failure() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateDupContainer", 4, "none", FS_EXT4, true)); + + assertEquals(StorageResultCode.OperationFailedInternalError, + createContainer("testCreateDupContainer", 4, "none", FS_EXT4, true)); + } + + public void test_Fat_Internal_CreateDuplicate_Failure() throws Exception { + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateDupContainer", 4, "none", FS_FAT, false)); + + assertEquals(StorageResultCode.OperationFailedInternalError, + createContainer("testCreateDupContainer", 4, "none", FS_FAT, false)); + } + + public void test_Ext4_Internal_CreateDuplicate_Failure() throws Exception { + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testCreateDupContainer", 4, "none", FS_EXT4, false)); + + assertEquals(StorageResultCode.OperationFailedInternalError, + createContainer("testCreateDupContainer", 4, "none", FS_EXT4, false)); + } + + + /* + * DESTROY + */ + + public void test_Fat_External_Destroy_Success() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testDestroyContainer", 4, "none", FS_FAT, true)); + assertEquals(StorageResultCode.OperationSucceeded, + destroyContainer("testDestroyContainer", false)); + } + + public void test_Ext4_External_Destroy_Success() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testDestroyContainer", 4, "none", FS_EXT4, true)); + assertEquals(StorageResultCode.OperationSucceeded, + destroyContainer("testDestroyContainer", false)); + } + + public void test_Fat_Internal_Destroy_Success() throws Exception { + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testDestroyContainer", 4, "none", FS_FAT, false)); + assertEquals(StorageResultCode.OperationSucceeded, + destroyContainer("testDestroyContainer", false)); + } + + public void test_Ext4_Internal_Destroy_Success() throws Exception { + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testDestroyContainer", 4, "none", FS_EXT4, false)); + assertEquals(StorageResultCode.OperationSucceeded, + destroyContainer("testDestroyContainer", false)); + } + + + /* + * MOUNT + */ + + public void test_Fat_External_Mount() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testMountContainer", 4, "none", FS_FAT, true)); + + assertEquals(StorageResultCode.OperationSucceeded, + unmountContainer("testMountContainer", false)); + + assertEquals(StorageResultCode.OperationSucceeded, + mountContainer("testMountContainer", "none")); + } + + + /* + * MOUNT BAD KEY - FAIL CASE + */ + + public void test_Fat_External_MountBadKey_Failure() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testMountBadKey", 4, "00000000000000000000000000000000", FS_FAT, + true)); + + assertEquals(StorageResultCode.OperationSucceeded, + unmountContainer("testMountBadKey", false)); + + assertEquals(StorageResultCode.OperationFailedInternalError, + mountContainer("testMountContainer", "000000000000000000000000000000001")); + + assertEquals(StorageResultCode.OperationFailedInternalError, + mountContainer("testMountContainer", "none")); + } + + + public void test_Fat_External_UnmountBusy_Success() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + IMountService ms = getMs(); + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testUnmountBusyContainer", 4, "none", FS_FAT, true)); + + String path = ms.getSecureContainerPath(SECURE_CONTAINER_PREFIX + + "testUnmountBusyContainer"); + + File f = new File(path, "reference"); + FileOutputStream fos = new FileOutputStream(f); + + assertEquals(StorageResultCode.OperationFailedStorageBusy, + unmountContainer("testUnmountBusyContainer", false)); + + fos.close(); + assertEquals(StorageResultCode.OperationSucceeded, + unmountContainer("testUnmountBusyContainer", false)); + } + + public void test_Fat_External_DestroyBusy() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + IMountService ms = getMs(); + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testDestroyBusyContainer", 4, "none", FS_FAT, true)); + + String path = ms.getSecureContainerPath(SECURE_CONTAINER_PREFIX + + "testDestroyBusyContainer"); + + File f = new File(path, "reference"); + FileOutputStream fos = new FileOutputStream(f); + + assertEquals(StorageResultCode.OperationFailedStorageBusy, + destroyContainer("testDestroyBusyContainer", false)); + + fos.close(); + assertEquals(StorageResultCode.OperationSucceeded, + destroyContainer("testDestroyBusyContainer", false)); + } + + public void test_Fat_External_Rename_Success() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testRenameContainer.1", 4, "none", FS_FAT, true)); + + assertEquals(StorageResultCode.OperationSucceeded, + unmountContainer("testRenameContainer.1", false)); + + assertEquals(StorageResultCode.OperationSucceeded, + renameContainer("testRenameContainer.1", "testRenameContainer.2")); + + assertFalse(containerExists("testRenameContainer.1")); + assertTrue(containerExists("testRenameContainer.2")); + } + + public void test_Fat_External_RenameSrcMounted_Failure() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testRenameContainer.1", 4, "none", FS_FAT, true)); + + assertEquals(StorageResultCode.OperationFailedStorageMounted, + renameContainer("testRenameContainer.1", "testRenameContainer.2")); + } + + public void test_Fat_External_RenameDstMounted_Failure() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testRenameContainer.1", 4, "none", FS_FAT, true)); + + assertEquals(StorageResultCode.OperationSucceeded, + unmountContainer("testRenameContainer.1", false)); + + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testRenameContainer.2", 4, "none", FS_FAT, true)); + + assertEquals(StorageResultCode.OperationFailedStorageMounted, + renameContainer("testRenameContainer.1", "testRenameContainer.2")); + } + + public void test_Fat_External_Size_Success() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + IMountService ms = getMs(); + assertEquals(StorageResultCode.OperationSucceeded, + createContainer("testContainerSize", 1, "none", FS_FAT, true)); + String path = ms.getSecureContainerPath(SECURE_CONTAINER_PREFIX + "testContainerSize"); + + byte[] buf = new byte[4096]; + File f = new File(path, "reference"); + FileOutputStream fos = new FileOutputStream(f); + for (int i = 0; i < (1024 * 1024); i += buf.length) { + fos.write(buf); + } + fos.close(); + } + + public void testGetSecureContainerPath_NonExistPath_Failure() throws Exception { + IMountService ms = getMs(); + assertNull("Getting the path for an invalid container should return null", + ms.getSecureContainerPath("jparks.broke.it")); + } + + /*------------ Tests for unmounting volume ---*/ + public final long MAX_WAIT_TIME=120*1000; + public final long WAIT_TIME_INCR=20*1000; + + boolean getMediaState() throws Exception { + String mPath = Environment.getExternalStorageDirectory().toString(); + String state = getMs().getVolumeState(mPath); + return Environment.MEDIA_MOUNTED.equals(state); + } + + boolean mountMedia() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return true; + } + + if (getMediaState()) { + return true; + } + + String mPath = Environment.getExternalStorageDirectory().toString(); + int ret = getMs().mountVolume(mPath); + return ret == StorageResultCode.OperationSucceeded; + } + + class StorageListener extends StorageEventListener { + String oldState; + String newState; + String path; + private boolean doneFlag = false; + + public void action() { + synchronized (this) { + doneFlag = true; + notifyAll(); + } + } + + public boolean isDone() { + return doneFlag; + } + + @Override + public void onStorageStateChanged(String path, String oldState, String newState) { + if (localLOGV) Log.i(TAG, "Storage state changed from " + oldState + " to " + newState); + this.oldState = oldState; + this.newState = newState; + this.path = path; + action(); + } + } + + private void unmountMedia() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + if (!getMediaState()) { + return; + } + + String path = Environment.getExternalStorageDirectory().toString(); + StorageListener observer = new StorageListener(); + StorageManager sm = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE); + sm.registerListener(observer); + try { + // Wait on observer + synchronized(observer) { + getMs().unmountVolume(path, false, false); + long waitTime = 0; + while((!observer.isDone()) && (waitTime < MAX_WAIT_TIME) ) { + observer.wait(WAIT_TIME_INCR); + waitTime += WAIT_TIME_INCR; + } + if(!observer.isDone()) { + fail("Timed out waiting for packageInstalled callback"); + } + } + } finally { + sm.unregisterListener(observer); + } + } + + public void testUnmount() throws Exception { + boolean oldStatus = getMediaState(); + Log.i(TAG, "oldStatus="+oldStatus); + try { + // Mount media firsts + if (!getMediaState()) { + mountMedia(); + } + unmountMedia(); + } finally { + // Restore old status + boolean currStatus = getMediaState(); + if (oldStatus != currStatus) { + if (oldStatus) { + // Mount media + mountMedia(); + } else { + unmountMedia(); + } + } + } + } + + class MultipleStorageLis extends StorageListener { + int count = 0; + public void onStorageStateChanged(String path, String oldState, String newState) { + count++; + super.action(); + } + } + /* + * This test invokes unmount multiple time and expects the call back + * to be invoked just once. + */ + public void testUnmountMultiple() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + boolean oldStatus = getMediaState(); + StorageManager sm = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE); + MultipleStorageLis observer = new MultipleStorageLis(); + try { + // Mount media firsts + if (!getMediaState()) { + mountMedia(); + } + String path = Environment.getExternalStorageDirectory().toString(); + sm.registerListener(observer); + // Wait on observer + synchronized(observer) { + for (int i = 0; i < 5; i++) { + getMs().unmountVolume(path, false, false); + } + long waitTime = 0; + while((!observer.isDone()) && (waitTime < MAX_WAIT_TIME) ) { + observer.wait(WAIT_TIME_INCR); + waitTime += WAIT_TIME_INCR; + } + if(!observer.isDone()) { + fail("Timed out waiting for packageInstalled callback"); + } + } + assertEquals(observer.count, 1); + } finally { + sm.unregisterListener(observer); + // Restore old status + boolean currStatus = getMediaState(); + if (oldStatus != currStatus) { + if (oldStatus) { + // Mount media + mountMedia(); + } else { + unmountMedia(); + } + } + } + } + + class ShutdownObserver extends IMountShutdownObserver.Stub{ + private boolean doneFlag = false; + int statusCode; + + public void action() { + synchronized (this) { + doneFlag = true; + notifyAll(); + } + } + + public boolean isDone() { + return doneFlag; + } + public void onShutDownComplete(int statusCode) throws RemoteException { + this.statusCode = statusCode; + action(); + } + + } + + void invokeShutdown() throws Exception { + IMountService ms = getMs(); + ShutdownObserver observer = new ShutdownObserver(); + synchronized (observer) { + ms.shutdown(observer); + } + } + + public void testShutdown() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + boolean oldStatus = getMediaState(); + try { + // Mount media firsts + if (!getMediaState()) { + mountMedia(); + } + invokeShutdown(); + } finally { + // Restore old status + boolean currStatus = getMediaState(); + if (oldStatus != currStatus) { + if (oldStatus) { + // Mount media + mountMedia(); + } else { + unmountMedia(); + } + } + } + } + + /* + * This test invokes unmount multiple time and expects the call back + * to be invoked just once. + */ + public void testShutdownMultiple() throws Exception { + if (Environment.isExternalStorageEmulated()) { + return; + } + + boolean oldStatus = getMediaState(); + try { + // Mount media firsts + if (!getMediaState()) { + mountMedia(); + } + IMountService ms = getMs(); + ShutdownObserver observer = new ShutdownObserver(); + synchronized (observer) { + ms.shutdown(observer); + for (int i = 0; i < 4; i++) { + ms.shutdown(null); + } + } + } finally { + // Restore old status + boolean currStatus = getMediaState(); + if (oldStatus != currStatus) { + if (oldStatus) { + // Mount media + mountMedia(); + } else { + unmountMedia(); + } + } + } + } + +} diff --git a/src/main/java/android/os/storage/IMountService.java b/src/main/java/android/os/storage/IMountService.java new file mode 100644 index 0000000..116110e --- /dev/null +++ b/src/main/java/android/os/storage/IMountService.java @@ -0,0 +1,1683 @@ +/* + * Copyright (C) 2010 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 android.os.storage; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; + +/** + * WARNING! Update IMountService.h and IMountService.cpp if you change this + * file. In particular, the ordering of the methods below must match the + * _TRANSACTION enum in IMountService.cpp + * + * @hide - Applications should use android.os.storage.StorageManager to access + * storage functions. + */ +public interface IMountService extends IInterface { + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends Binder implements IMountService { + private static class Proxy implements IMountService { + private final IBinder mRemote; + + Proxy(IBinder remote) { + mRemote = remote; + } + + public IBinder asBinder() { + return mRemote; + } + + public String getInterfaceDescriptor() { + return DESCRIPTOR; + } + + /** + * Registers an IMountServiceListener for receiving async + * notifications. + */ + public void registerListener(IMountServiceListener listener) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeStrongBinder((listener != null ? listener.asBinder() : null)); + mRemote.transact(Stub.TRANSACTION_registerListener, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + /** + * Unregisters an IMountServiceListener + */ + public void unregisterListener(IMountServiceListener listener) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeStrongBinder((listener != null ? listener.asBinder() : null)); + mRemote.transact(Stub.TRANSACTION_unregisterListener, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + /** + * Returns true if a USB mass storage host is connected + */ + public boolean isUsbMassStorageConnected() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_isUsbMassStorageConnected, _data, _reply, 0); + _reply.readException(); + _result = 0 != _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /** + * Enables / disables USB mass storage. The caller should check + * actual status of enabling/disabling USB mass storage via + * StorageEventListener. + */ + public void setUsbMassStorageEnabled(boolean enable) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt((enable ? 1 : 0)); + mRemote.transact(Stub.TRANSACTION_setUsbMassStorageEnabled, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + /** + * Returns true if a USB mass storage host is enabled (media is + * shared) + */ + public boolean isUsbMassStorageEnabled() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_isUsbMassStorageEnabled, _data, _reply, 0); + _reply.readException(); + _result = 0 != _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /** + * Mount external storage at given mount point. Returns an int + * consistent with MountServiceResultCode + */ + public int mountVolume(String mountPoint) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(mountPoint); + mRemote.transact(Stub.TRANSACTION_mountVolume, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /** + * Safely unmount external storage at given mount point. The unmount + * is an asynchronous operation. Applications should register + * StorageEventListener for storage related status changes. + */ + public void unmountVolume(String mountPoint, boolean force, boolean removeEncryption) + throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(mountPoint); + _data.writeInt((force ? 1 : 0)); + _data.writeInt((removeEncryption ? 1 : 0)); + mRemote.transact(Stub.TRANSACTION_unmountVolume, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + /** + * Format external storage given a mount point. Returns an int + * consistent with MountServiceResultCode + */ + public int formatVolume(String mountPoint) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(mountPoint); + mRemote.transact(Stub.TRANSACTION_formatVolume, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /** + * Returns an array of pids with open files on the specified path. + */ + public int[] getStorageUsers(String path) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int[] _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(path); + mRemote.transact(Stub.TRANSACTION_getStorageUsers, _data, _reply, 0); + _reply.readException(); + _result = _reply.createIntArray(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /** + * Gets the state of a volume via its mountpoint. + */ + public String getVolumeState(String mountPoint) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(mountPoint); + mRemote.transact(Stub.TRANSACTION_getVolumeState, _data, _reply, 0); + _reply.readException(); + _result = _reply.readString(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /* + * Creates a secure container with the specified parameters. Returns + * an int consistent with MountServiceResultCode + */ + public int createSecureContainer(String id, int sizeMb, String fstype, String key, + int ownerUid, boolean external) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(id); + _data.writeInt(sizeMb); + _data.writeString(fstype); + _data.writeString(key); + _data.writeInt(ownerUid); + _data.writeInt(external ? 1 : 0); + mRemote.transact(Stub.TRANSACTION_createSecureContainer, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /* + * Destroy a secure container, and free up all resources associated + * with it. NOTE: Ensure all references are released prior to + * deleting. Returns an int consistent with MountServiceResultCode + */ + public int destroySecureContainer(String id, boolean force) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(id); + _data.writeInt((force ? 1 : 0)); + mRemote.transact(Stub.TRANSACTION_destroySecureContainer, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /* + * Finalize a container which has just been created and populated. + * After finalization, the container is immutable. Returns an int + * consistent with MountServiceResultCode + */ + public int finalizeSecureContainer(String id) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(id); + mRemote.transact(Stub.TRANSACTION_finalizeSecureContainer, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /* + * Mount a secure container with the specified key and owner UID. + * Returns an int consistent with MountServiceResultCode + */ + public int mountSecureContainer(String id, String key, int ownerUid, boolean readOnly) + throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(id); + _data.writeString(key); + _data.writeInt(ownerUid); + _data.writeInt(readOnly ? 1 : 0); + mRemote.transact(Stub.TRANSACTION_mountSecureContainer, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /* + * Unount a secure container. Returns an int consistent with + * MountServiceResultCode + */ + public int unmountSecureContainer(String id, boolean force) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(id); + _data.writeInt((force ? 1 : 0)); + mRemote.transact(Stub.TRANSACTION_unmountSecureContainer, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /* + * Returns true if the specified container is mounted + */ + public boolean isSecureContainerMounted(String id) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(id); + mRemote.transact(Stub.TRANSACTION_isSecureContainerMounted, _data, _reply, 0); + _reply.readException(); + _result = 0 != _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /* + * Rename an unmounted secure container. Returns an int consistent + * with MountServiceResultCode + */ + public int renameSecureContainer(String oldId, String newId) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(oldId); + _data.writeString(newId); + mRemote.transact(Stub.TRANSACTION_renameSecureContainer, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /* + * Returns the filesystem path of a mounted secure container. + */ + public String getSecureContainerPath(String id) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(id); + mRemote.transact(Stub.TRANSACTION_getSecureContainerPath, _data, _reply, 0); + _reply.readException(); + _result = _reply.readString(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /** + * Gets an Array of currently known secure container IDs + */ + public String[] getSecureContainerList() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + String[] _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_getSecureContainerList, _data, _reply, 0); + _reply.readException(); + _result = _reply.createStringArray(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /** + * Shuts down the MountService and gracefully unmounts all external + * media. Invokes call back once the shutdown is complete. + */ + public void shutdown(IMountShutdownObserver observer) + throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeStrongBinder((observer != null ? observer.asBinder() : null)); + mRemote.transact(Stub.TRANSACTION_shutdown, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + /** + * Call into MountService by PackageManager to notify that its done + * processing the media status update request. + */ + public void finishMediaUpdate() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_finishMediaUpdate, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + /** + * Mounts an Opaque Binary Blob (OBB) with the specified decryption + * key and only allows the calling process's UID access to the + * contents. MountService will call back to the supplied + * IObbActionListener to inform it of the terminal state of the + * call. + */ + public void mountObb(String rawPath, String canonicalPath, String key, + IObbActionListener token, int nonce) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(rawPath); + _data.writeString(canonicalPath); + _data.writeString(key); + _data.writeStrongBinder((token != null ? token.asBinder() : null)); + _data.writeInt(nonce); + mRemote.transact(Stub.TRANSACTION_mountObb, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + /** + * Unmounts an Opaque Binary Blob (OBB). When the force flag is + * specified, any program using it will be forcibly killed to + * unmount the image. MountService will call back to the supplied + * IObbActionListener to inform it of the terminal state of the + * call. + */ + public void unmountObb( + String rawPath, boolean force, IObbActionListener token, int nonce) + throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(rawPath); + _data.writeInt((force ? 1 : 0)); + _data.writeStrongBinder((token != null ? token.asBinder() : null)); + _data.writeInt(nonce); + mRemote.transact(Stub.TRANSACTION_unmountObb, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + /** + * Checks whether the specified Opaque Binary Blob (OBB) is mounted + * somewhere. + */ + public boolean isObbMounted(String rawPath) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(rawPath); + mRemote.transact(Stub.TRANSACTION_isObbMounted, _data, _reply, 0); + _reply.readException(); + _result = 0 != _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /** + * Gets the path to the mounted Opaque Binary Blob (OBB). + */ + public String getMountedObbPath(String rawPath) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(rawPath); + mRemote.transact(Stub.TRANSACTION_getMountedObbPath, _data, _reply, 0); + _reply.readException(); + _result = _reply.readString(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /** + * Returns whether the external storage is emulated. + */ + public boolean isExternalStorageEmulated() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_isExternalStorageEmulated, _data, _reply, 0); + _reply.readException(); + _result = 0 != _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public int getEncryptionState() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_getEncryptionState, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public int decryptStorage(String password) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(password); + mRemote.transact(Stub.TRANSACTION_decryptStorage, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public int encryptStorage(int type, String password) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(type); + _data.writeString(password); + mRemote.transact(Stub.TRANSACTION_encryptStorage, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public int changeEncryptionPassword(int type, String password) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(type); + _data.writeString(password); + mRemote.transact(Stub.TRANSACTION_changeEncryptionPassword, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public int verifyEncryptionPassword(String password) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(password); + mRemote.transact(Stub.TRANSACTION_verifyEncryptionPassword, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public int getPasswordType() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_getPasswordType, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public String getPassword() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_getPassword, _data, _reply, 0); + _reply.readException(); + _result = _reply.readString(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public void clearPassword() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_clearPassword, _data, _reply, IBinder.FLAG_ONEWAY); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + public void setField(String field, String data) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(field); + _data.writeString(data); + mRemote.transact(Stub.TRANSACTION_setField, _data, _reply, IBinder.FLAG_ONEWAY); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + public String getField(String field) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(field); + mRemote.transact(Stub.TRANSACTION_getField, _data, _reply, 0); + _reply.readException(); + _result = _reply.readString(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public StorageVolume[] getVolumeList() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + StorageVolume[] _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_getVolumeList, _data, _reply, 0); + _reply.readException(); + _result = _reply.createTypedArray(StorageVolume.CREATOR); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /* + * Returns the filesystem path of a mounted secure container. + */ + public String getSecureContainerFilesystemPath(String id) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(id); + mRemote.transact(Stub.TRANSACTION_getSecureContainerFilesystemPath, _data, _reply, 0); + _reply.readException(); + _result = _reply.readString(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + /** + * Fix permissions in a container which has just been created and + * populated. Returns an int consistent with MountServiceResultCode + */ + public int fixPermissionsSecureContainer(String id, int gid, String filename) + throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(id); + _data.writeInt(gid); + _data.writeString(filename); + mRemote.transact(Stub.TRANSACTION_fixPermissionsSecureContainer, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public int mkdirs(String callingPkg, String path) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(callingPkg); + _data.writeString(path); + mRemote.transact(Stub.TRANSACTION_mkdirs, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public int resizeSecureContainer(String id, int sizeMb, String key) + throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(id); + _data.writeInt(sizeMb); + _data.writeString(key); + mRemote.transact(Stub.TRANSACTION_resizeSecureContainer, _data, _reply, 0); + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public long lastMaintenance() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + long _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_lastMaintenance, _data, _reply, 0); + _reply.readException(); + _result = _reply.readLong(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public void runMaintenance() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_runMaintenance, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return; + } + } + + private static final String DESCRIPTOR = "IMountService"; + + static final int TRANSACTION_registerListener = IBinder.FIRST_CALL_TRANSACTION + 0; + + static final int TRANSACTION_unregisterListener = IBinder.FIRST_CALL_TRANSACTION + 1; + + static final int TRANSACTION_isUsbMassStorageConnected = IBinder.FIRST_CALL_TRANSACTION + 2; + + static final int TRANSACTION_setUsbMassStorageEnabled = IBinder.FIRST_CALL_TRANSACTION + 3; + + static final int TRANSACTION_isUsbMassStorageEnabled = IBinder.FIRST_CALL_TRANSACTION + 4; + + static final int TRANSACTION_mountVolume = IBinder.FIRST_CALL_TRANSACTION + 5; + + static final int TRANSACTION_unmountVolume = IBinder.FIRST_CALL_TRANSACTION + 6; + + static final int TRANSACTION_formatVolume = IBinder.FIRST_CALL_TRANSACTION + 7; + + static final int TRANSACTION_getStorageUsers = IBinder.FIRST_CALL_TRANSACTION + 8; + + static final int TRANSACTION_getVolumeState = IBinder.FIRST_CALL_TRANSACTION + 9; + + static final int TRANSACTION_createSecureContainer = IBinder.FIRST_CALL_TRANSACTION + 10; + + static final int TRANSACTION_finalizeSecureContainer = IBinder.FIRST_CALL_TRANSACTION + 11; + + static final int TRANSACTION_destroySecureContainer = IBinder.FIRST_CALL_TRANSACTION + 12; + + static final int TRANSACTION_mountSecureContainer = IBinder.FIRST_CALL_TRANSACTION + 13; + + static final int TRANSACTION_unmountSecureContainer = IBinder.FIRST_CALL_TRANSACTION + 14; + + static final int TRANSACTION_isSecureContainerMounted = IBinder.FIRST_CALL_TRANSACTION + 15; + + static final int TRANSACTION_renameSecureContainer = IBinder.FIRST_CALL_TRANSACTION + 16; + + static final int TRANSACTION_getSecureContainerPath = IBinder.FIRST_CALL_TRANSACTION + 17; + + static final int TRANSACTION_getSecureContainerList = IBinder.FIRST_CALL_TRANSACTION + 18; + + static final int TRANSACTION_shutdown = IBinder.FIRST_CALL_TRANSACTION + 19; + + static final int TRANSACTION_finishMediaUpdate = IBinder.FIRST_CALL_TRANSACTION + 20; + + static final int TRANSACTION_mountObb = IBinder.FIRST_CALL_TRANSACTION + 21; + + static final int TRANSACTION_unmountObb = IBinder.FIRST_CALL_TRANSACTION + 22; + + static final int TRANSACTION_isObbMounted = IBinder.FIRST_CALL_TRANSACTION + 23; + + static final int TRANSACTION_getMountedObbPath = IBinder.FIRST_CALL_TRANSACTION + 24; + + static final int TRANSACTION_isExternalStorageEmulated = IBinder.FIRST_CALL_TRANSACTION + 25; + + static final int TRANSACTION_decryptStorage = IBinder.FIRST_CALL_TRANSACTION + 26; + + static final int TRANSACTION_encryptStorage = IBinder.FIRST_CALL_TRANSACTION + 27; + + static final int TRANSACTION_changeEncryptionPassword = IBinder.FIRST_CALL_TRANSACTION + 28; + + static final int TRANSACTION_getVolumeList = IBinder.FIRST_CALL_TRANSACTION + 29; + + static final int TRANSACTION_getSecureContainerFilesystemPath = IBinder.FIRST_CALL_TRANSACTION + 30; + + static final int TRANSACTION_getEncryptionState = IBinder.FIRST_CALL_TRANSACTION + 31; + + static final int TRANSACTION_verifyEncryptionPassword = IBinder.FIRST_CALL_TRANSACTION + 32; + + static final int TRANSACTION_fixPermissionsSecureContainer = IBinder.FIRST_CALL_TRANSACTION + 33; + + static final int TRANSACTION_mkdirs = IBinder.FIRST_CALL_TRANSACTION + 34; + + static final int TRANSACTION_getPasswordType = IBinder.FIRST_CALL_TRANSACTION + 35; + + static final int TRANSACTION_getPassword = IBinder.FIRST_CALL_TRANSACTION + 36; + + static final int TRANSACTION_clearPassword = IBinder.FIRST_CALL_TRANSACTION + 37; + + static final int TRANSACTION_setField = IBinder.FIRST_CALL_TRANSACTION + 38; + + static final int TRANSACTION_getField = IBinder.FIRST_CALL_TRANSACTION + 39; + + static final int TRANSACTION_resizeSecureContainer = IBinder.FIRST_CALL_TRANSACTION + 40; + + static final int TRANSACTION_lastMaintenance = IBinder.FIRST_CALL_TRANSACTION + 41; + + static final int TRANSACTION_runMaintenance = IBinder.FIRST_CALL_TRANSACTION + 42; + + /** + * Cast an IBinder object into an IMountService interface, generating a + * proxy if needed. + */ + public static IMountService asInterface(IBinder obj) { + if (obj == null) { + return null; + } + IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (iin != null && iin instanceof IMountService) { + return (IMountService) iin; + } + return new IMountService.Stub.Proxy(obj); + } + + /** Construct the stub at attach it to the interface. */ + public Stub() { + attachInterface(this, DESCRIPTOR); + } + + public IBinder asBinder() { + return this; + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, + int flags) throws RemoteException { + switch (code) { + case INTERFACE_TRANSACTION: { + reply.writeString(DESCRIPTOR); + return true; + } + case TRANSACTION_registerListener: { + data.enforceInterface(DESCRIPTOR); + IMountServiceListener listener; + listener = IMountServiceListener.Stub.asInterface(data.readStrongBinder()); + registerListener(listener); + reply.writeNoException(); + return true; + } + case TRANSACTION_unregisterListener: { + data.enforceInterface(DESCRIPTOR); + IMountServiceListener listener; + listener = IMountServiceListener.Stub.asInterface(data.readStrongBinder()); + unregisterListener(listener); + reply.writeNoException(); + return true; + } + case TRANSACTION_isUsbMassStorageConnected: { + data.enforceInterface(DESCRIPTOR); + boolean result = isUsbMassStorageConnected(); + reply.writeNoException(); + reply.writeInt((result ? 1 : 0)); + return true; + } + case TRANSACTION_setUsbMassStorageEnabled: { + data.enforceInterface(DESCRIPTOR); + boolean enable; + enable = 0 != data.readInt(); + setUsbMassStorageEnabled(enable); + reply.writeNoException(); + return true; + } + case TRANSACTION_isUsbMassStorageEnabled: { + data.enforceInterface(DESCRIPTOR); + boolean result = isUsbMassStorageEnabled(); + reply.writeNoException(); + reply.writeInt((result ? 1 : 0)); + return true; + } + case TRANSACTION_mountVolume: { + data.enforceInterface(DESCRIPTOR); + String mountPoint; + mountPoint = data.readString(); + int resultCode = mountVolume(mountPoint); + reply.writeNoException(); + reply.writeInt(resultCode); + return true; + } + case TRANSACTION_unmountVolume: { + data.enforceInterface(DESCRIPTOR); + String mountPoint; + mountPoint = data.readString(); + boolean force = 0 != data.readInt(); + boolean removeEncrypt = 0 != data.readInt(); + unmountVolume(mountPoint, force, removeEncrypt); + reply.writeNoException(); + return true; + } + case TRANSACTION_formatVolume: { + data.enforceInterface(DESCRIPTOR); + String mountPoint; + mountPoint = data.readString(); + int result = formatVolume(mountPoint); + reply.writeNoException(); + reply.writeInt(result); + return true; + } + case TRANSACTION_getStorageUsers: { + data.enforceInterface(DESCRIPTOR); + String path; + path = data.readString(); + int[] pids = getStorageUsers(path); + reply.writeNoException(); + reply.writeIntArray(pids); + return true; + } + case TRANSACTION_getVolumeState: { + data.enforceInterface(DESCRIPTOR); + String mountPoint; + mountPoint = data.readString(); + String state = getVolumeState(mountPoint); + reply.writeNoException(); + reply.writeString(state); + return true; + } + case TRANSACTION_createSecureContainer: { + data.enforceInterface(DESCRIPTOR); + String id; + id = data.readString(); + int sizeMb; + sizeMb = data.readInt(); + String fstype; + fstype = data.readString(); + String key; + key = data.readString(); + int ownerUid; + ownerUid = data.readInt(); + boolean external; + external = 0 != data.readInt(); + int resultCode = createSecureContainer(id, sizeMb, fstype, key, ownerUid, + external); + reply.writeNoException(); + reply.writeInt(resultCode); + return true; + } + case TRANSACTION_finalizeSecureContainer: { + data.enforceInterface(DESCRIPTOR); + String id; + id = data.readString(); + int resultCode = finalizeSecureContainer(id); + reply.writeNoException(); + reply.writeInt(resultCode); + return true; + } + case TRANSACTION_destroySecureContainer: { + data.enforceInterface(DESCRIPTOR); + String id; + id = data.readString(); + boolean force; + force = 0 != data.readInt(); + int resultCode = destroySecureContainer(id, force); + reply.writeNoException(); + reply.writeInt(resultCode); + return true; + } + case TRANSACTION_mountSecureContainer: { + data.enforceInterface(DESCRIPTOR); + String id; + id = data.readString(); + String key; + key = data.readString(); + int ownerUid; + ownerUid = data.readInt(); + boolean readOnly; + readOnly = data.readInt() != 0; + int resultCode = mountSecureContainer(id, key, ownerUid, readOnly); + reply.writeNoException(); + reply.writeInt(resultCode); + return true; + } + case TRANSACTION_unmountSecureContainer: { + data.enforceInterface(DESCRIPTOR); + String id; + id = data.readString(); + boolean force; + force = 0 != data.readInt(); + int resultCode = unmountSecureContainer(id, force); + reply.writeNoException(); + reply.writeInt(resultCode); + return true; + } + case TRANSACTION_isSecureContainerMounted: { + data.enforceInterface(DESCRIPTOR); + String id; + id = data.readString(); + boolean status = isSecureContainerMounted(id); + reply.writeNoException(); + reply.writeInt((status ? 1 : 0)); + return true; + } + case TRANSACTION_renameSecureContainer: { + data.enforceInterface(DESCRIPTOR); + String oldId; + oldId = data.readString(); + String newId; + newId = data.readString(); + int resultCode = renameSecureContainer(oldId, newId); + reply.writeNoException(); + reply.writeInt(resultCode); + return true; + } + case TRANSACTION_getSecureContainerPath: { + data.enforceInterface(DESCRIPTOR); + String id; + id = data.readString(); + String path = getSecureContainerPath(id); + reply.writeNoException(); + reply.writeString(path); + return true; + } + case TRANSACTION_getSecureContainerList: { + data.enforceInterface(DESCRIPTOR); + String[] ids = getSecureContainerList(); + reply.writeNoException(); + reply.writeStringArray(ids); + return true; + } + case TRANSACTION_shutdown: { + data.enforceInterface(DESCRIPTOR); + IMountShutdownObserver observer; + observer = IMountShutdownObserver.Stub.asInterface(data + .readStrongBinder()); + shutdown(observer); + reply.writeNoException(); + return true; + } + case TRANSACTION_finishMediaUpdate: { + data.enforceInterface(DESCRIPTOR); + finishMediaUpdate(); + reply.writeNoException(); + return true; + } + case TRANSACTION_mountObb: { + data.enforceInterface(DESCRIPTOR); + final String rawPath = data.readString(); + final String canonicalPath = data.readString(); + final String key = data.readString(); + IObbActionListener observer; + observer = IObbActionListener.Stub.asInterface(data.readStrongBinder()); + int nonce; + nonce = data.readInt(); + mountObb(rawPath, canonicalPath, key, observer, nonce); + reply.writeNoException(); + return true; + } + case TRANSACTION_unmountObb: { + data.enforceInterface(DESCRIPTOR); + String filename; + filename = data.readString(); + boolean force; + force = 0 != data.readInt(); + IObbActionListener observer; + observer = IObbActionListener.Stub.asInterface(data.readStrongBinder()); + int nonce; + nonce = data.readInt(); + unmountObb(filename, force, observer, nonce); + reply.writeNoException(); + return true; + } + case TRANSACTION_isObbMounted: { + data.enforceInterface(DESCRIPTOR); + String filename; + filename = data.readString(); + boolean status = isObbMounted(filename); + reply.writeNoException(); + reply.writeInt((status ? 1 : 0)); + return true; + } + case TRANSACTION_getMountedObbPath: { + data.enforceInterface(DESCRIPTOR); + String filename; + filename = data.readString(); + String mountedPath = getMountedObbPath(filename); + reply.writeNoException(); + reply.writeString(mountedPath); + return true; + } + case TRANSACTION_isExternalStorageEmulated: { + data.enforceInterface(DESCRIPTOR); + boolean emulated = isExternalStorageEmulated(); + reply.writeNoException(); + reply.writeInt(emulated ? 1 : 0); + return true; + } + case TRANSACTION_decryptStorage: { + data.enforceInterface(DESCRIPTOR); + String password = data.readString(); + int result = decryptStorage(password); + reply.writeNoException(); + reply.writeInt(result); + return true; + } + case TRANSACTION_encryptStorage: { + data.enforceInterface(DESCRIPTOR); + int type = data.readInt(); + String password = data.readString(); + int result = encryptStorage(type, password); + reply.writeNoException(); + reply.writeInt(result); + return true; + } + case TRANSACTION_changeEncryptionPassword: { + data.enforceInterface(DESCRIPTOR); + int type = data.readInt(); + String password = data.readString(); + int result = changeEncryptionPassword(type, password); + reply.writeNoException(); + reply.writeInt(result); + return true; + } + case TRANSACTION_getVolumeList: { + data.enforceInterface(DESCRIPTOR); + StorageVolume[] result = getVolumeList(); + reply.writeNoException(); + reply.writeTypedArray(result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + return true; + } + case TRANSACTION_getSecureContainerFilesystemPath: { + data.enforceInterface(DESCRIPTOR); + String id; + id = data.readString(); + String path = getSecureContainerFilesystemPath(id); + reply.writeNoException(); + reply.writeString(path); + return true; + } + case TRANSACTION_getEncryptionState: { + data.enforceInterface(DESCRIPTOR); + int result = getEncryptionState(); + reply.writeNoException(); + reply.writeInt(result); + return true; + } + case TRANSACTION_fixPermissionsSecureContainer: { + data.enforceInterface(DESCRIPTOR); + String id; + id = data.readString(); + int gid; + gid = data.readInt(); + String filename; + filename = data.readString(); + int resultCode = fixPermissionsSecureContainer(id, gid, filename); + reply.writeNoException(); + reply.writeInt(resultCode); + return true; + } + case TRANSACTION_mkdirs: { + data.enforceInterface(DESCRIPTOR); + String callingPkg = data.readString(); + String path = data.readString(); + int result = mkdirs(callingPkg, path); + reply.writeNoException(); + reply.writeInt(result); + return true; + } + case TRANSACTION_getPasswordType: { + data.enforceInterface(DESCRIPTOR); + int result = getPasswordType(); + reply.writeNoException(); + reply.writeInt(result); + return true; + } + case TRANSACTION_getPassword: { + data.enforceInterface(DESCRIPTOR); + String result = getPassword(); + reply.writeNoException(); + reply.writeString(result); + return true; + } + case TRANSACTION_clearPassword: { + data.enforceInterface(DESCRIPTOR); + clearPassword(); + reply.writeNoException(); + return true; + } + case TRANSACTION_setField: { + data.enforceInterface(DESCRIPTOR); + String field = data.readString(); + String contents = data.readString(); + setField(field, contents); + reply.writeNoException(); + return true; + } + case TRANSACTION_getField: { + data.enforceInterface(DESCRIPTOR); + String field = data.readString(); + String contents = getField(field); + reply.writeNoException(); + reply.writeString(contents); + return true; + } + case TRANSACTION_resizeSecureContainer: { + data.enforceInterface(DESCRIPTOR); + String id; + id = data.readString(); + int sizeMb; + sizeMb = data.readInt(); + String key; + key = data.readString(); + int resultCode = resizeSecureContainer(id, sizeMb, key); + reply.writeNoException(); + reply.writeInt(resultCode); + return true; + } + case TRANSACTION_lastMaintenance: { + data.enforceInterface(DESCRIPTOR); + long lastMaintenance = lastMaintenance(); + reply.writeNoException(); + reply.writeLong(lastMaintenance); + return true; + } + case TRANSACTION_runMaintenance: { + data.enforceInterface(DESCRIPTOR); + runMaintenance(); + reply.writeNoException(); + return true; + } + } + return super.onTransact(code, data, reply, flags); + } + } + + /* + * Creates a secure container with the specified parameters. Returns an int + * consistent with MountServiceResultCode + */ + public int createSecureContainer(String id, int sizeMb, String fstype, String key, + int ownerUid, boolean external) throws RemoteException; + + /* + * Destroy a secure container, and free up all resources associated with it. + * NOTE: Ensure all references are released prior to deleting. Returns an + * int consistent with MountServiceResultCode + */ + public int destroySecureContainer(String id, boolean force) throws RemoteException; + + /* + * Finalize a container which has just been created and populated. After + * finalization, the container is immutable. Returns an int consistent with + * MountServiceResultCode + */ + public int finalizeSecureContainer(String id) throws RemoteException; + + /** + * Call into MountService by PackageManager to notify that its done + * processing the media status update request. + */ + public void finishMediaUpdate() throws RemoteException; + + /** + * Format external storage given a mount point. Returns an int consistent + * with MountServiceResultCode + */ + public int formatVolume(String mountPoint) throws RemoteException; + + /** + * Gets the path to the mounted Opaque Binary Blob (OBB). + */ + public String getMountedObbPath(String rawPath) throws RemoteException; + + /** + * Gets an Array of currently known secure container IDs + */ + public String[] getSecureContainerList() throws RemoteException; + + /* + * Returns the filesystem path of a mounted secure container. + */ + public String getSecureContainerPath(String id) throws RemoteException; + + /** + * Returns an array of pids with open files on the specified path. + */ + public int[] getStorageUsers(String path) throws RemoteException; + + /** + * Gets the state of a volume via its mountpoint. + */ + public String getVolumeState(String mountPoint) throws RemoteException; + + /** + * Checks whether the specified Opaque Binary Blob (OBB) is mounted + * somewhere. + */ + public boolean isObbMounted(String rawPath) throws RemoteException; + + /* + * Returns true if the specified container is mounted + */ + public boolean isSecureContainerMounted(String id) throws RemoteException; + + /** + * Returns true if a USB mass storage host is connected + */ + public boolean isUsbMassStorageConnected() throws RemoteException; + + /** + * Returns true if a USB mass storage host is enabled (media is shared) + */ + public boolean isUsbMassStorageEnabled() throws RemoteException; + + /** + * Mounts an Opaque Binary Blob (OBB) with the specified decryption key and + * only allows the calling process's UID access to the contents. + * MountService will call back to the supplied IObbActionListener to inform + * it of the terminal state of the call. + */ + public void mountObb(String rawPath, String canonicalPath, String key, + IObbActionListener token, int nonce) throws RemoteException; + + /* + * Mount a secure container with the specified key and owner UID. Returns an + * int consistent with MountServiceResultCode + */ + public int mountSecureContainer(String id, String key, int ownerUid, boolean readOnly) + throws RemoteException; + + /** + * Mount external storage at given mount point. Returns an int consistent + * with MountServiceResultCode + */ + public int mountVolume(String mountPoint) throws RemoteException; + + /** + * Registers an IMountServiceListener for receiving async notifications. + */ + public void registerListener(IMountServiceListener listener) throws RemoteException; + + /* + * Rename an unmounted secure container. Returns an int consistent with + * MountServiceResultCode + */ + public int renameSecureContainer(String oldId, String newId) throws RemoteException; + + /** + * Enables / disables USB mass storage. The caller should check actual + * status of enabling/disabling USB mass storage via StorageEventListener. + */ + public void setUsbMassStorageEnabled(boolean enable) throws RemoteException; + + /** + * Shuts down the MountService and gracefully unmounts all external media. + * Invokes call back once the shutdown is complete. + */ + public void shutdown(IMountShutdownObserver observer) throws RemoteException; + + /** + * Unmounts an Opaque Binary Blob (OBB). When the force flag is specified, + * any program using it will be forcibly killed to unmount the image. + * MountService will call back to the supplied IObbActionListener to inform + * it of the terminal state of the call. + */ + public void unmountObb(String rawPath, boolean force, IObbActionListener token, int nonce) + throws RemoteException; + + /* + * Unount a secure container. Returns an int consistent with + * MountServiceResultCode + */ + public int unmountSecureContainer(String id, boolean force) throws RemoteException; + + /** + * Safely unmount external storage at given mount point. The unmount is an + * asynchronous operation. Applications should register StorageEventListener + * for storage related status changes. + * @param mountPoint the mount point + * @param force whether or not to forcefully unmount it (e.g. even if programs are using this + * data currently) + * @param removeEncryption whether or not encryption mapping should be removed from the volume. + * This value implies {@code force}. + */ + public void unmountVolume(String mountPoint, boolean force, boolean removeEncryption) + throws RemoteException; + + /** + * Unregisters an IMountServiceListener + */ + public void unregisterListener(IMountServiceListener listener) throws RemoteException; + + /** + * Returns whether or not the external storage is emulated. + */ + public boolean isExternalStorageEmulated() throws RemoteException; + + /** The volume is not encrypted. */ + static final int ENCRYPTION_STATE_NONE = 1; + /** The volume has been encrypted succesfully. */ + static final int ENCRYPTION_STATE_OK = 0; + /** The volume is in a bad state.*/ + static final int ENCRYPTION_STATE_ERROR_UNKNOWN = -1; + /** Encryption is incomplete */ + static final int ENCRYPTION_STATE_ERROR_INCOMPLETE = -2; + /** Encryption is incomplete and irrecoverable */ + static final int ENCRYPTION_STATE_ERROR_INCONSISTENT = -3; + /** Underlying data is corrupt */ + static final int ENCRYPTION_STATE_ERROR_CORRUPT = -4; + + /** + * Determines the encryption state of the volume. + * @return a numerical value. See {@code ENCRYPTION_STATE_*} for possible values. + */ + public int getEncryptionState() throws RemoteException; + + /** + * Decrypts any encrypted volumes. + */ + public int decryptStorage(String password) throws RemoteException; + + /** + * Encrypts storage. + */ + public int encryptStorage(int type, String password) throws RemoteException; + + /** + * Changes the encryption password. + */ + public int changeEncryptionPassword(int type, String password) + throws RemoteException; + + /** + * Verify the encryption password against the stored volume. This method + * may only be called by the system process. + */ + public int verifyEncryptionPassword(String password) throws RemoteException; + + /** + * Returns list of all mountable volumes. + */ + public StorageVolume[] getVolumeList() throws RemoteException; + + /** + * Gets the path on the filesystem for the ASEC container itself. + * + * @param cid ASEC container ID + * @return path to filesystem or {@code null} if it's not found + * @throws RemoteException + */ + public String getSecureContainerFilesystemPath(String cid) throws RemoteException; + + /* + * Fix permissions in a container which has just been created and populated. + * Returns an int consistent with MountServiceResultCode + */ + public int fixPermissionsSecureContainer(String id, int gid, String filename) + throws RemoteException; + + /** + * Ensure that all directories along given path exist, creating parent + * directories as needed. Validates that given path is absolute and that it + * contains no relative "." or ".." paths or symlinks. Also ensures that + * path belongs to a volume managed by vold, and that path is either + * external storage data or OBB directory belonging to calling app. + */ + public int mkdirs(String callingPkg, String path) throws RemoteException; + + /** + * Determines the type of the encryption password + * @return PasswordType + */ + public int getPasswordType() throws RemoteException; + + /** + * Get password from vold + * @return password or empty string + */ + public String getPassword() throws RemoteException; + + /** + * Securely clear password from vold + */ + public void clearPassword() throws RemoteException; + + /** + * Set a field in the crypto header. + * @param field field to set + * @param contents contents to set in field + */ + public void setField(String field, String contents) throws RemoteException; + + /** + * Gets a field from the crypto header. + * @param field field to get + * @return contents of field + */ + public String getField(String field) throws RemoteException; + + public int resizeSecureContainer(String id, int sizeMb, String key) throws RemoteException; + + /** + * Report the time of the last maintenance operation such as fstrim. + * @return Timestamp of the last maintenance operation, in the + * System.currentTimeMillis() time base + * @throws RemoteException + */ + public long lastMaintenance() throws RemoteException; + + /** + * Kick off an immediate maintenance operation + * @throws RemoteException + */ + public void runMaintenance() throws RemoteException; +} diff --git a/src/main/java/android/os/storage/IMountServiceListener.java b/src/main/java/android/os/storage/IMountServiceListener.java new file mode 100644 index 0000000..d5c5fa5 --- /dev/null +++ b/src/main/java/android/os/storage/IMountServiceListener.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2009 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 android.os.storage; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; + +/** + * Callback class for receiving events from MountService. + * + * @hide - Applications should use IStorageEventListener for storage event + * callbacks. + */ +public interface IMountServiceListener extends IInterface { + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends Binder implements IMountServiceListener { + private static final String DESCRIPTOR = "IMountServiceListener"; + + /** Construct the stub at attach it to the interface. */ + public Stub() { + this.attachInterface(this, DESCRIPTOR); + } + + /** + * Cast an IBinder object into an IMountServiceListener interface, + * generating a proxy if needed. + */ + public static IMountServiceListener asInterface(IBinder obj) { + if ((obj == null)) { + return null; + } + IInterface iin = (IInterface) obj.queryLocalInterface(DESCRIPTOR); + if (((iin != null) && (iin instanceof IMountServiceListener))) { + return ((IMountServiceListener) iin); + } + return new IMountServiceListener.Stub.Proxy(obj); + } + + public IBinder asBinder() { + return this; + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + switch (code) { + case INTERFACE_TRANSACTION: { + reply.writeString(DESCRIPTOR); + return true; + } + case TRANSACTION_onUsbMassStorageConnectionChanged: { + data.enforceInterface(DESCRIPTOR); + boolean connected; + connected = (0 != data.readInt()); + this.onUsbMassStorageConnectionChanged(connected); + reply.writeNoException(); + return true; + } + case TRANSACTION_onStorageStateChanged: { + data.enforceInterface(DESCRIPTOR); + String path; + path = data.readString(); + String oldState; + oldState = data.readString(); + String newState; + newState = data.readString(); + this.onStorageStateChanged(path, oldState, newState); + reply.writeNoException(); + return true; + } + } + return super.onTransact(code, data, reply, flags); + } + + private static class Proxy implements IMountServiceListener { + private IBinder mRemote; + + Proxy(IBinder remote) { + mRemote = remote; + } + + public IBinder asBinder() { + return mRemote; + } + + public String getInterfaceDescriptor() { + return DESCRIPTOR; + } + + /** + * Detection state of USB Mass Storage has changed + * + * @param available true if a UMS host is connected. + */ + public void onUsbMassStorageConnectionChanged(boolean connected) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(((connected) ? (1) : (0))); + mRemote.transact(Stub.TRANSACTION_onUsbMassStorageConnectionChanged, _data, + _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + /** + * Storage state has changed. + * + * @param path The volume mount path. + * @param oldState The old state of the volume. + * @param newState The new state of the volume. Note: State is one + * of the values returned by + * Environment.getExternalStorageState() + */ + public void onStorageStateChanged(String path, String oldState, String newState) + throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(path); + _data.writeString(oldState); + _data.writeString(newState); + mRemote.transact(Stub.TRANSACTION_onStorageStateChanged, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + } + + static final int TRANSACTION_onUsbMassStorageConnectionChanged = (IBinder.FIRST_CALL_TRANSACTION + 0); + + static final int TRANSACTION_onStorageStateChanged = (IBinder.FIRST_CALL_TRANSACTION + 1); + } + + /** + * Detection state of USB Mass Storage has changed + * + * @param available true if a UMS host is connected. + */ + public void onUsbMassStorageConnectionChanged(boolean connected) throws RemoteException; + + /** + * Storage state has changed. + * + * @param path The volume mount path. + * @param oldState The old state of the volume. + * @param newState The new state of the volume. Note: State is one of the + * values returned by Environment.getExternalStorageState() + */ + public void onStorageStateChanged(String path, String oldState, String newState) + throws RemoteException; +} diff --git a/src/main/java/android/os/storage/IMountShutdownObserver.java b/src/main/java/android/os/storage/IMountShutdownObserver.java new file mode 100644 index 0000000..d946e1a --- /dev/null +++ b/src/main/java/android/os/storage/IMountShutdownObserver.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2009 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 android.os.storage; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; + +/** + * Callback class for receiving events related to shutdown. + * + * @hide - For internal consumption only. + */ +public interface IMountShutdownObserver extends IInterface { + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends Binder implements IMountShutdownObserver { + private static final java.lang.String DESCRIPTOR = "IMountShutdownObserver"; + + /** Construct the stub at attach it to the interface. */ + public Stub() { + this.attachInterface(this, DESCRIPTOR); + } + + /** + * Cast an IBinder object into an IMountShutdownObserver interface, + * generating a proxy if needed. + */ + public static IMountShutdownObserver asInterface(IBinder obj) { + if ((obj == null)) { + return null; + } + IInterface iin = (IInterface) obj.queryLocalInterface(DESCRIPTOR); + if (((iin != null) && (iin instanceof IMountShutdownObserver))) { + return ((IMountShutdownObserver) iin); + } + return new IMountShutdownObserver.Stub.Proxy(obj); + } + + public IBinder asBinder() { + return this; + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + switch (code) { + case INTERFACE_TRANSACTION: { + reply.writeString(DESCRIPTOR); + return true; + } + case TRANSACTION_onShutDownComplete: { + data.enforceInterface(DESCRIPTOR); + int statusCode; + statusCode = data.readInt(); + this.onShutDownComplete(statusCode); + reply.writeNoException(); + return true; + } + } + return super.onTransact(code, data, reply, flags); + } + + private static class Proxy implements IMountShutdownObserver { + private IBinder mRemote; + + Proxy(IBinder remote) { + mRemote = remote; + } + + public IBinder asBinder() { + return mRemote; + } + + public java.lang.String getInterfaceDescriptor() { + return DESCRIPTOR; + } + + /** + * This method is called when the shutdown of MountService + * completed. + * + * @param statusCode indicates success or failure of the shutdown. + */ + public void onShutDownComplete(int statusCode) throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(statusCode); + mRemote.transact(Stub.TRANSACTION_onShutDownComplete, _data, _reply, 0); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + } + + static final int TRANSACTION_onShutDownComplete = (IBinder.FIRST_CALL_TRANSACTION + 0); + } + + /** + * This method is called when the shutdown of MountService completed. + * + * @param statusCode indicates success or failure of the shutdown. + */ + public void onShutDownComplete(int statusCode) throws RemoteException; +} diff --git a/src/main/java/android/os/storage/IObbActionListener.java b/src/main/java/android/os/storage/IObbActionListener.java new file mode 100644 index 0000000..35da4b0 --- /dev/null +++ b/src/main/java/android/os/storage/IObbActionListener.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2010 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 android.os.storage; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; + +/** + * Callback class for receiving events from MountService about Opaque Binary + * Blobs (OBBs). + * + * @hide - Applications should use StorageManager to interact with OBBs. + */ +public interface IObbActionListener extends IInterface { + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends Binder implements IObbActionListener { + private static final String DESCRIPTOR = "IObbActionListener"; + + /** Construct the stub at attach it to the interface. */ + public Stub() { + this.attachInterface(this, DESCRIPTOR); + } + + /** + * Cast an IBinder object into an IObbActionListener interface, + * generating a proxy if needed. + */ + public static IObbActionListener asInterface(IBinder obj) { + if ((obj == null)) { + return null; + } + IInterface iin = (IInterface) obj.queryLocalInterface(DESCRIPTOR); + if (((iin != null) && (iin instanceof IObbActionListener))) { + return ((IObbActionListener) iin); + } + return new IObbActionListener.Stub.Proxy(obj); + } + + public IBinder asBinder() { + return this; + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + switch (code) { + case INTERFACE_TRANSACTION: { + reply.writeString(DESCRIPTOR); + return true; + } + case TRANSACTION_onObbResult: { + data.enforceInterface(DESCRIPTOR); + String filename; + filename = data.readString(); + int nonce; + nonce = data.readInt(); + int status; + status = data.readInt(); + this.onObbResult(filename, nonce, status); + reply.writeNoException(); + return true; + } + } + return super.onTransact(code, data, reply, flags); + } + + private static class Proxy implements IObbActionListener { + private IBinder mRemote; + + Proxy(IBinder remote) { + mRemote = remote; + } + + public IBinder asBinder() { + return mRemote; + } + + public String getInterfaceDescriptor() { + return DESCRIPTOR; + } + + /** + * Return from an OBB action result. + * + * @param filename the path to the OBB the operation was performed + * on + * @param returnCode status of the operation + */ + public void onObbResult(String filename, int nonce, int status) + throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(filename); + _data.writeInt(nonce); + _data.writeInt(status); + mRemote.transact(Stub.TRANSACTION_onObbResult, _data, _reply, + android.os.IBinder.FLAG_ONEWAY); + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + } + + static final int TRANSACTION_onObbResult = (IBinder.FIRST_CALL_TRANSACTION + 0); + } + + /** + * Return from an OBB action result. + * + * @param filename the path to the OBB the operation was performed on + * @param nonce identifier that is meaningful to the receiver + * @param status status code as defined in {@link OnObbStateChangeListener} + */ + public void onObbResult(String filename, int nonce, int status) throws RemoteException; +} diff --git a/src/main/java/android/os/storage/MountServiceListener.java b/src/main/java/android/os/storage/MountServiceListener.java new file mode 100644 index 0000000..bebb3f6 --- /dev/null +++ b/src/main/java/android/os/storage/MountServiceListener.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 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 android.os.storage; + +/** + * Callback class for receiving progress reports during a restore operation. These + * methods will all be called on your application's main thread. + * @hide + */ +public abstract class MountServiceListener { + /** + * USB Mass storage connection state has changed. + * + * @param connected True if UMS is connected. + */ + void onUsbMassStorageConnectionChanged(boolean connected) { + } + + /** + * Storage state has changed. + * + * @param path The volume mount path. + * @param oldState The old state of the volume. + * @param newState The new state of the volume. + * + * @Note: State is one of the values returned by Environment.getExternalStorageState() + */ + void onStorageStateChange(String path, String oldState, String newState) { + } +} diff --git a/src/main/java/android/os/storage/OnObbStateChangeListener.java b/src/main/java/android/os/storage/OnObbStateChangeListener.java new file mode 100644 index 0000000..1fb1782 --- /dev/null +++ b/src/main/java/android/os/storage/OnObbStateChangeListener.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2010 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 android.os.storage; + +/** + * Used for receiving notifications from {@link StorageManager} about OBB file + * states. + */ +public abstract class OnObbStateChangeListener { + + /** + * The OBB container is now mounted and ready for use. Returned in status + * messages from calls made via {@link StorageManager} + */ + public static final int MOUNTED = 1; + + /** + * The OBB container is now unmounted and not usable. Returned in status + * messages from calls made via {@link StorageManager} + */ + public static final int UNMOUNTED = 2; + + /** + * There was an internal system error encountered while trying to mount the + * OBB. Returned in status messages from calls made via + * {@link StorageManager} + */ + public static final int ERROR_INTERNAL = 20; + + /** + * The OBB could not be mounted by the system. Returned in status messages + * from calls made via {@link StorageManager} + */ + public static final int ERROR_COULD_NOT_MOUNT = 21; + + /** + * The OBB could not be unmounted. This most likely indicates that a file is + * in use on the OBB. Returned in status messages from calls made via + * {@link StorageManager} + */ + public static final int ERROR_COULD_NOT_UNMOUNT = 22; + + /** + * A call was made to unmount the OBB when it was not mounted. Returned in + * status messages from calls made via {@link StorageManager} + */ + public static final int ERROR_NOT_MOUNTED = 23; + + /** + * The OBB has already been mounted. Returned in status messages from calls + * made via {@link StorageManager} + */ + public static final int ERROR_ALREADY_MOUNTED = 24; + + /** + * The current application does not have permission to use this OBB. This + * could be because the OBB indicates it's owned by a different package or + * some other error. Returned in status messages from calls made via + * {@link StorageManager} + */ + public static final int ERROR_PERMISSION_DENIED = 25; + + /** + * Called when an OBB has changed states. + * + * @param path path to the OBB file the state change has happened on + * @param state the current state of the OBB + */ + public void onObbStateChange(String path, int state) { + } +} diff --git a/src/main/java/android/os/storage/StorageEventListener.java b/src/main/java/android/os/storage/StorageEventListener.java new file mode 100644 index 0000000..6c73d04 --- /dev/null +++ b/src/main/java/android/os/storage/StorageEventListener.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 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 android.os.storage; + +/** + * Used for receiving notifications from the StorageManager + * + * @hide + */ +public abstract class StorageEventListener { + /** + * Called when the detection state of a USB Mass Storage host has changed. + * @param connected true if the USB mass storage is connected. + */ + public void onUsbMassStorageConnectionChanged(boolean connected) { + } + + /** + * Called when storage has changed state + * @param path the filesystem path for the storage + * @param oldState the old state as returned by {@link android.os.Environment#getExternalStorageState()}. + * @param newState the old state as returned by {@link android.os.Environment#getExternalStorageState()}. + */ + public void onStorageStateChanged(String path, String oldState, String newState) { + } +} diff --git a/src/main/java/android/os/storage/StorageListener.java b/src/main/java/android/os/storage/StorageListener.java new file mode 100644 index 0000000..6a26b88 --- /dev/null +++ b/src/main/java/android/os/storage/StorageListener.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 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 android.os.storage; + +import android.util.Log; + +public class StorageListener extends StorageEventListener { + private static final boolean localLOGV = true; + + public static final String TAG = "StorageListener"; + + private String mTargetState; + private boolean doneFlag = false; + + public StorageListener(String targetState) { + mTargetState = targetState; + } + + @Override + public void onStorageStateChanged(String path, String oldState, String newState) { + if (localLOGV) Log.i(TAG, "Storage state changed from " + oldState + " to " + newState); + + synchronized (this) { + if (mTargetState.equals(newState)) { + doneFlag = true; + notifyAll(); + } + } + } + + public boolean isDone() { + return doneFlag; + } +} diff --git a/src/main/java/android/os/storage/StorageManager.java b/src/main/java/android/os/storage/StorageManager.java new file mode 100644 index 0000000..2785ee8 --- /dev/null +++ b/src/main/java/android/os/storage/StorageManager.java @@ -0,0 +1,676 @@ +/* + * Copyright (C) 2008 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 android.os.storage; + +import static android.net.TrafficStats.MB_IN_BYTES; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.Settings; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.util.Preconditions; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * StorageManager is the interface to the systems storage service. The storage + * manager handles storage-related items such as Opaque Binary Blobs (OBBs). + *

    + * OBBs contain a filesystem that maybe be encrypted on disk and mounted + * on-demand from an application. OBBs are a good way of providing large amounts + * of binary assets without packaging them into APKs as they may be multiple + * gigabytes in size. However, due to their size, they're most likely stored in + * a shared storage pool accessible from all programs. The system does not + * guarantee the security of the OBB file itself: if any program modifies the + * OBB, there is no guarantee that a read from that OBB will produce the + * expected output. + *

    + * Get an instance of this class by calling + * {@link android.content.Context#getSystemService(java.lang.String)} with an + * argument of {@link android.content.Context#STORAGE_SERVICE}. + */ +public class StorageManager { + private static final String TAG = "StorageManager"; + + private final ContentResolver mResolver; + + /* + * Our internal MountService binder reference + */ + private final IMountService mMountService; + + /* + * The looper target for callbacks + */ + private final Looper mTgtLooper; + + /* + * Target listener for binder callbacks + */ + private MountServiceBinderListener mBinderListener; + + /* + * List of our listeners + */ + private List mListeners = new ArrayList(); + + /* + * Next available nonce + */ + final private AtomicInteger mNextNonce = new AtomicInteger(0); + + private class MountServiceBinderListener extends IMountServiceListener.Stub { + public void onUsbMassStorageConnectionChanged(boolean available) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + mListeners.get(i).sendShareAvailabilityChanged(available); + } + } + + public void onStorageStateChanged(String path, String oldState, String newState) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + mListeners.get(i).sendStorageStateChanged(path, oldState, newState); + } + } + } + + /** + * Binder listener for OBB action results. + */ + private final ObbActionListener mObbActionListener = new ObbActionListener(); + + private class ObbActionListener extends IObbActionListener.Stub { + @SuppressWarnings("hiding") + private SparseArray mListeners = new SparseArray(); + + @Override + public void onObbResult(String filename, int nonce, int status) { + final ObbListenerDelegate delegate; + synchronized (mListeners) { + delegate = mListeners.get(nonce); + if (delegate != null) { + mListeners.remove(nonce); + } + } + + if (delegate != null) { + delegate.sendObbStateChanged(filename, status); + } + } + + public int addListener(OnObbStateChangeListener listener) { + final ObbListenerDelegate delegate = new ObbListenerDelegate(listener); + + synchronized (mListeners) { + mListeners.put(delegate.nonce, delegate); + } + + return delegate.nonce; + } + } + + private int getNextNonce() { + return mNextNonce.getAndIncrement(); + } + + /** + * Private class containing sender and receiver code for StorageEvents. + */ + private class ObbListenerDelegate { + private final WeakReference mObbEventListenerRef; + private final Handler mHandler; + + private final int nonce; + + ObbListenerDelegate(OnObbStateChangeListener listener) { + nonce = getNextNonce(); + mObbEventListenerRef = new WeakReference(listener); + mHandler = new Handler(mTgtLooper) { + @Override + public void handleMessage(Message msg) { + final OnObbStateChangeListener changeListener = getListener(); + if (changeListener == null) { + return; + } + + StorageEvent e = (StorageEvent) msg.obj; + + if (msg.what == StorageEvent.EVENT_OBB_STATE_CHANGED) { + ObbStateChangedStorageEvent ev = (ObbStateChangedStorageEvent) e; + changeListener.onObbStateChange(ev.path, ev.state); + } else { + Log.e(TAG, "Unsupported event " + msg.what); + } + } + }; + } + + OnObbStateChangeListener getListener() { + if (mObbEventListenerRef == null) { + return null; + } + return mObbEventListenerRef.get(); + } + + void sendObbStateChanged(String path, int state) { + ObbStateChangedStorageEvent e = new ObbStateChangedStorageEvent(path, state); + mHandler.sendMessage(e.getMessage()); + } + } + + /** + * Message sent during an OBB status change event. + */ + private class ObbStateChangedStorageEvent extends StorageEvent { + public final String path; + + public final int state; + + public ObbStateChangedStorageEvent(String path, int state) { + super(EVENT_OBB_STATE_CHANGED); + this.path = path; + this.state = state; + } + } + + /** + * Private base class for messages sent between the callback thread + * and the target looper handler. + */ + private class StorageEvent { + static final int EVENT_UMS_CONNECTION_CHANGED = 1; + static final int EVENT_STORAGE_STATE_CHANGED = 2; + static final int EVENT_OBB_STATE_CHANGED = 3; + + private Message mMessage; + + public StorageEvent(int what) { + mMessage = Message.obtain(); + mMessage.what = what; + mMessage.obj = this; + } + + public Message getMessage() { + return mMessage; + } + } + + /** + * Message sent on a USB mass storage connection change. + */ + private class UmsConnectionChangedStorageEvent extends StorageEvent { + public boolean available; + + public UmsConnectionChangedStorageEvent(boolean a) { + super(EVENT_UMS_CONNECTION_CHANGED); + available = a; + } + } + + /** + * Message sent on volume state change. + */ + private class StorageStateChangedStorageEvent extends StorageEvent { + public String path; + public String oldState; + public String newState; + + public StorageStateChangedStorageEvent(String p, String oldS, String newS) { + super(EVENT_STORAGE_STATE_CHANGED); + path = p; + oldState = oldS; + newState = newS; + } + } + + /** + * Private class containing sender and receiver code for StorageEvents. + */ + private class ListenerDelegate { + final StorageEventListener mStorageEventListener; + private final Handler mHandler; + + ListenerDelegate(StorageEventListener listener) { + mStorageEventListener = listener; + mHandler = new Handler(mTgtLooper) { + @Override + public void handleMessage(Message msg) { + StorageEvent e = (StorageEvent) msg.obj; + + if (msg.what == StorageEvent.EVENT_UMS_CONNECTION_CHANGED) { + UmsConnectionChangedStorageEvent ev = (UmsConnectionChangedStorageEvent) e; + mStorageEventListener.onUsbMassStorageConnectionChanged(ev.available); + } else if (msg.what == StorageEvent.EVENT_STORAGE_STATE_CHANGED) { + StorageStateChangedStorageEvent ev = (StorageStateChangedStorageEvent) e; + mStorageEventListener.onStorageStateChanged(ev.path, ev.oldState, ev.newState); + } else { + Log.e(TAG, "Unsupported event " + msg.what); + } + } + }; + } + + StorageEventListener getListener() { + return mStorageEventListener; + } + + void sendShareAvailabilityChanged(boolean available) { + UmsConnectionChangedStorageEvent e = new UmsConnectionChangedStorageEvent(available); + mHandler.sendMessage(e.getMessage()); + } + + void sendStorageStateChanged(String path, String oldState, String newState) { + StorageStateChangedStorageEvent e = new StorageStateChangedStorageEvent(path, oldState, newState); + mHandler.sendMessage(e.getMessage()); + } + } + + /** {@hide} */ + public static StorageManager from(Context context) { + return (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + } + + /** + * Constructs a StorageManager object through which an application can + * can communicate with the systems mount service. + * + * @param tgtLooper The {@link android.os.Looper} which events will be received on. + * + *

    Applications can get instance of this class by calling + * {@link android.content.Context#getSystemService(java.lang.String)} with an argument + * of {@link android.content.Context#STORAGE_SERVICE}. + * + * @hide + */ + public StorageManager(ContentResolver resolver, Looper tgtLooper) throws RemoteException { + mResolver = resolver; + mTgtLooper = tgtLooper; + mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); + if (mMountService == null) { + Log.e(TAG, "Unable to connect to mount service! - is it running yet?"); + return; + } + } + + /** + * Registers a {@link android.os.storage.StorageEventListener StorageEventListener}. + * + * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. + * + * @hide + */ + public void registerListener(StorageEventListener listener) { + if (listener == null) { + return; + } + + synchronized (mListeners) { + if (mBinderListener == null ) { + try { + mBinderListener = new MountServiceBinderListener(); + mMountService.registerListener(mBinderListener); + } catch (RemoteException rex) { + Log.e(TAG, "Register mBinderListener failed"); + return; + } + } + mListeners.add(new ListenerDelegate(listener)); + } + } + + /** + * Unregisters a {@link android.os.storage.StorageEventListener StorageEventListener}. + * + * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. + * + * @hide + */ + public void unregisterListener(StorageEventListener listener) { + if (listener == null) { + return; + } + + synchronized (mListeners) { + final int size = mListeners.size(); + for (int i=0 ; ikey is + * specified, it is supplied to the mounting process to be used in any + * encryption used in the OBB. + *

    + * The OBB will remain mounted for as long as the StorageManager reference + * is held by the application. As soon as this reference is lost, the OBBs + * in use will be unmounted. The {@link OnObbStateChangeListener} registered + * with this call will receive the success or failure of this operation. + *

    + * Note: you can only mount OBB files for which the OBB tag on the + * file matches a package ID that is owned by the calling program's UID. + * That is, shared UID applications can attempt to mount any other + * application's OBB that shares its UID. + * + * @param rawPath the path to the OBB file + * @param key secret used to encrypt the OBB; may be null if no + * encryption was used on the OBB. + * @param listener will receive the success or failure of the operation + * @return whether the mount call was successfully queued or not + */ + public boolean mountObb(String rawPath, String key, OnObbStateChangeListener listener) { + Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); + Preconditions.checkNotNull(listener, "listener cannot be null"); + + try { + final String canonicalPath = new File(rawPath).getCanonicalPath(); + final int nonce = mObbActionListener.addListener(listener); + mMountService.mountObb(rawPath, canonicalPath, key, mObbActionListener, nonce); + return true; + } catch (IOException e) { + throw new IllegalArgumentException("Failed to resolve path: " + rawPath, e); + } catch (RemoteException e) { + Log.e(TAG, "Failed to mount OBB", e); + } + + return false; + } + + /** + * Unmount an Opaque Binary Blob (OBB) file asynchronously. If the + * force flag is true, it will kill any application needed to + * unmount the given OBB (even the calling application). + *

    + * The {@link OnObbStateChangeListener} registered with this call will + * receive the success or failure of this operation. + *

    + * Note: you can only mount OBB files for which the OBB tag on the + * file matches a package ID that is owned by the calling program's UID. + * That is, shared UID applications can obtain access to any other + * application's OBB that shares its UID. + *

    + * + * @param rawPath path to the OBB file + * @param force whether to kill any programs using this in order to unmount + * it + * @param listener will receive the success or failure of the operation + * @return whether the unmount call was successfully queued or not + */ + public boolean unmountObb(String rawPath, boolean force, OnObbStateChangeListener listener) { + Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); + Preconditions.checkNotNull(listener, "listener cannot be null"); + + try { + final int nonce = mObbActionListener.addListener(listener); + mMountService.unmountObb(rawPath, force, mObbActionListener, nonce); + return true; + } catch (RemoteException e) { + Log.e(TAG, "Failed to mount OBB", e); + } + + return false; + } + + /** + * Check whether an Opaque Binary Blob (OBB) is mounted or not. + * + * @param rawPath path to OBB image + * @return true if OBB is mounted; false if not mounted or on error + */ + public boolean isObbMounted(String rawPath) { + Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); + + try { + return mMountService.isObbMounted(rawPath); + } catch (RemoteException e) { + Log.e(TAG, "Failed to check if OBB is mounted", e); + } + + return false; + } + + /** + * Check the mounted path of an Opaque Binary Blob (OBB) file. This will + * give you the path to where you can obtain access to the internals of the + * OBB. + * + * @param rawPath path to OBB image + * @return absolute path to mounted OBB image data or null if + * not mounted or exception encountered trying to read status + */ + public String getMountedObbPath(String rawPath) { + Preconditions.checkNotNull(rawPath, "rawPath cannot be null"); + + try { + return mMountService.getMountedObbPath(rawPath); + } catch (RemoteException e) { + Log.e(TAG, "Failed to find mounted path for OBB", e); + } + + return null; + } + + /** + * Gets the state of a volume via its mountpoint. + * @hide + */ + public String getVolumeState(String mountPoint) { + if (mMountService == null) return Environment.MEDIA_REMOVED; + try { + return mMountService.getVolumeState(mountPoint); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get volume state", e); + return null; + } + } + + /** + * Returns list of all mountable volumes. + * @hide + */ + public StorageVolume[] getVolumeList() { + if (mMountService == null) return new StorageVolume[0]; + try { + Parcelable[] list = mMountService.getVolumeList(); + if (list == null) return new StorageVolume[0]; + int length = list.length; + StorageVolume[] result = new StorageVolume[length]; + for (int i = 0; i < length; i++) { + result[i] = (StorageVolume)list[i]; + } + return result; + } catch (RemoteException e) { + Log.e(TAG, "Failed to get volume list", e); + return null; + } + } + + /** + * Returns list of paths for all mountable volumes. + * @hide + */ + public String[] getVolumePaths() { + StorageVolume[] volumes = getVolumeList(); + if (volumes == null) return null; + int count = volumes.length; + String[] paths = new String[count]; + for (int i = 0; i < count; i++) { + paths[i] = volumes[i].getPath(); + } + return paths; + } + + /** {@hide} */ + public StorageVolume getPrimaryVolume() { + return getPrimaryVolume(getVolumeList()); + } + + /** {@hide} */ + public static StorageVolume getPrimaryVolume(StorageVolume[] volumes) { + for (StorageVolume volume : volumes) { + if (volume.isPrimary()) { + return volume; + } + } + Log.w(TAG, "No primary storage defined"); + return null; + } + + private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10; + private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500 * MB_IN_BYTES; + private static final long DEFAULT_FULL_THRESHOLD_BYTES = MB_IN_BYTES; + + /** + * Return the number of available bytes until the given path is considered + * running low on storage. + * + * @hide + */ + public long getStorageBytesUntilLow(File path) { + return path.getUsableSpace() - getStorageFullBytes(path); + } + + /** + * Return the number of available bytes at which the given path is + * considered running low on storage. + * + * @hide + */ + public long getStorageLowBytes(File path) { + final long lowPercent = Settings.Global.getInt(mResolver, + Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE); + final long lowBytes = (path.getTotalSpace() * lowPercent) / 100; + + final long maxLowBytes = Settings.Global.getLong(mResolver, + Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES); + + return Math.min(lowBytes, maxLowBytes); + } + + /** + * Return the number of available bytes at which the given path is + * considered full. + * + * @hide + */ + public long getStorageFullBytes(File path) { + return Settings.Global.getLong(mResolver, Settings.Global.SYS_STORAGE_FULL_THRESHOLD_BYTES, + DEFAULT_FULL_THRESHOLD_BYTES); + } + + /// Consts to match the password types in cryptfs.h + /** @hide */ + public static final int CRYPT_TYPE_PASSWORD = 0; + /** @hide */ + public static final int CRYPT_TYPE_DEFAULT = 1; + /** @hide */ + public static final int CRYPT_TYPE_PATTERN = 2; + /** @hide */ + public static final int CRYPT_TYPE_PIN = 3; + + // Constants for the data available via MountService.getField. + /** @hide */ + public static final String SYSTEM_LOCALE_KEY = "SystemLocale"; + /** @hide */ + public static final String OWNER_INFO_KEY = "OwnerInfo"; + /** @hide */ + public static final String PATTERN_VISIBLE_KEY = "PatternVisible"; +} diff --git a/src/main/java/android/os/storage/StorageManagerBaseTest.java b/src/main/java/android/os/storage/StorageManagerBaseTest.java new file mode 100644 index 0000000..90cb9a5 --- /dev/null +++ b/src/main/java/android/os/storage/StorageManagerBaseTest.java @@ -0,0 +1,644 @@ +/* + * Copyright (C) 2010 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 android.os.storage; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.os.Environment; +import android.os.SystemClock; +import android.test.InstrumentationTestCase; +import android.util.Log; +import android.os.Environment; +import android.os.FileUtils; +import android.os.storage.OnObbStateChangeListener; +import android.os.storage.StorageManager; + +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.InputStream; +import java.io.IOException; +import java.io.StringReader; + +public class StorageManagerBaseTest extends InstrumentationTestCase { + + protected Context mContext = null; + protected StorageManager mSm = null; + private static String LOG_TAG = "StorageManagerBaseTest"; + protected static final long MAX_WAIT_TIME = 120*1000; + protected static final long WAIT_TIME_INCR = 5*1000; + protected static String OBB_FILE_1 = "obb_file1.obb"; + protected static String OBB_FILE_1_CONTENTS_1 = "OneToOneThousandInts.bin"; + protected static String OBB_FILE_2 = "obb_file2.obb"; + protected static String OBB_FILE_3 = "obb_file3.obb"; + protected static String OBB_FILE_1_PASSWORD = "password1"; + protected static String OBB_FILE_1_ENCRYPTED = "obb_enc_file100_orig1.obb"; + protected static String OBB_FILE_2_UNSIGNED = "obb_file2_nosign.obb"; + protected static String OBB_FILE_3_PASSWORD = "password3"; + protected static String OBB_FILE_3_ENCRYPTED = "obb_enc_file100_orig3.obb"; + protected static String OBB_FILE_3_BAD_PACKAGENAME = "obb_file3_bad_packagename.obb"; + + protected static boolean FORCE = true; + protected static boolean DONT_FORCE = false; + + private static final String SAMPLE1_TEXT = "This is sample text.\n\nTesting 1 2 3."; + + private static final String SAMPLE2_TEXT = + "We the people of the United States, in order to form a more perfect union,\n" + + "establish justice, insure domestic tranquility, provide for the common\n" + + "defense, promote the general welfare, and secure the blessings of liberty\n" + + "to ourselves and our posterity, do ordain and establish this Constitution\n" + + "for the United States of America.\n\n"; + + class MountingObbThread extends Thread { + boolean mStop = false; + volatile boolean mFileOpenOnObb = false; + private String mObbFilePath = null; + private String mPathToContentsFile = null; + private String mOfficialObbFilePath = null; + + /** + * Constructor + * + * @param obbFilePath path to the OBB image file + * @param pathToContentsFile path to a file on the mounted OBB volume to open after the OBB + * has been mounted + */ + public MountingObbThread (String obbFilePath, String pathToContentsFile) { + assertTrue("obbFilePath cannot be null!", obbFilePath != null); + mObbFilePath = obbFilePath; + assertTrue("path to contents file cannot be null!", pathToContentsFile != null); + mPathToContentsFile = pathToContentsFile; + } + + /** + * Runs the thread + * + * Mounts OBB_FILE_1, and tries to open a file on the mounted OBB (specified in the + * constructor). Once it's open, it waits until someone calls its doStop(), after which it + * closes the opened file. + */ + public void run() { + // the official OBB file path and the mount-request file path should be the same, but + // let's distinguish the two as they may make for some interesting tests later + mOfficialObbFilePath = mountObb(mObbFilePath); + assertEquals("Expected and actual OBB file paths differ!", mObbFilePath, + mOfficialObbFilePath); + + // open a file on OBB 1... + DataInputStream inputFile = openFileOnMountedObb(mOfficialObbFilePath, + mPathToContentsFile); + assertTrue("Failed to open file!", inputFile != null); + + synchronized (this) { + mFileOpenOnObb = true; + notifyAll(); + } + + while (!mStop) { + try { + Thread.sleep(WAIT_TIME_INCR); + } catch (InterruptedException e) { + // nothing special to be done for interruptions + } + } + try { + inputFile.close(); + } catch (IOException e) { + fail("Failed to close file on OBB due to error: " + e.toString()); + } + } + + /** + * Tells whether a file has yet been successfully opened on the OBB or not + * + * @return true if the specified file on the OBB was opened; false otherwise + */ + public boolean isFileOpenOnObb() { + return mFileOpenOnObb; + } + + /** + * Returns the official path of the OBB file that was mounted + * + * This is not the mount path, but the normalized path to the actual OBB file + * + * @return a {@link String} representation of the path to the OBB file that was mounted + */ + public String officialObbFilePath() { + return mOfficialObbFilePath; + } + + /** + * Requests the thread to stop running + * + * Closes the opened file and returns + */ + public void doStop() { + mStop = true; + } + } + + public class ObbListener extends OnObbStateChangeListener { + private String LOG_TAG = "StorageManagerBaseTest.ObbListener"; + + String mOfficialPath = null; + boolean mDone = false; + int mState = -1; + + /** + * {@inheritDoc} + */ + @Override + public void onObbStateChange(String path, int state) { + Log.i(LOG_TAG, "Storage state changing to: " + state); + + synchronized (this) { + Log.i(LOG_TAG, "OfficialPath is now: " + path); + mState = state; + mOfficialPath = path; + mDone = true; + notifyAll(); + } + } + + /** + * Tells whether we are done or not (system told us the OBB has changed state) + * + * @return true if the system has told us this OBB's state has changed, false otherwise + */ + public boolean isDone() { + return mDone; + } + + /** + * The last state of the OBB, according to the system + * + * @return A {@link String} representation of the state of the OBB + */ + public int state() { + return mState; + } + + /** + * The normalized, official path to the OBB file (according to the system) + * + * @return A {@link String} representation of the official path to the OBB file + */ + public String officialPath() { + return mOfficialPath; + } + } + + /** + * {@inheritDoc} + */ + @Override + public void setUp() throws Exception { + mContext = getInstrumentation().getContext(); + mSm = (StorageManager)mContext.getSystemService(android.content.Context.STORAGE_SERVICE); + + } + + /** + * Helper to copy a raw resource file to an actual specified file + * + * @param rawResId The raw resource ID of the OBB resource file + * @param outFile A File representing the file we want to copy the OBB to + * @throws NotFoundException If the resource file could not be found + */ + private void copyRawToFile(int rawResId, File outFile) throws NotFoundException { + Resources res = mContext.getResources(); + InputStream is = null; + try { + is = res.openRawResource(rawResId); + } catch (NotFoundException e) { + Log.i(LOG_TAG, "Failed to load resource with id: " + rawResId); + throw e; + } + FileUtils.setPermissions(outFile.getPath(), FileUtils.S_IRWXU | FileUtils.S_IRWXG + | FileUtils.S_IRWXO, -1, -1); + assertTrue(FileUtils.copyToFile(is, outFile)); + FileUtils.setPermissions(outFile.getPath(), FileUtils.S_IRWXU | FileUtils.S_IRWXG + | FileUtils.S_IRWXO, -1, -1); + } + + /** + * Creates an OBB file (with the given name), into the app's standard files directory + * + * @param name The name of the OBB file we want to create/write to + * @param rawResId The raw resource ID of the OBB file in the package + * @return A {@link File} representing the file to write to + */ + protected File createObbFile(String name, int rawResId) { + File outFile = null; + try { + final File filesDir = mContext.getFilesDir(); + outFile = new File(filesDir, name); + copyRawToFile(rawResId, outFile); + } catch (NotFoundException e) { + if (outFile != null) { + outFile.delete(); + } + } + return outFile; + } + + /** + * Mounts an OBB file and opens a file located on it + * + * @param obbPath Path to OBB image + * @param fileName The full name and path to the file on the OBB to open once the OBB is mounted + * @return The {@link DataInputStream} representing the opened file, if successful in opening + * the file, or null of unsuccessful. + */ + protected DataInputStream openFileOnMountedObb(String obbPath, String fileName) { + + // get mSm obb mount path + assertTrue("Cannot open file when OBB is not mounted!", mSm.isObbMounted(obbPath)); + + String path = mSm.getMountedObbPath(obbPath); + assertTrue("Path should not be null!", path != null); + + File inFile = new File(path, fileName); + DataInputStream inStream = null; + try { + inStream = new DataInputStream(new FileInputStream(inFile)); + Log.i(LOG_TAG, "Opened file: " + fileName + " for read at path: " + path); + } catch (FileNotFoundException e) { + Log.e(LOG_TAG, e.toString()); + return null; + } catch (SecurityException e) { + Log.e(LOG_TAG, e.toString()); + return null; + } + return inStream; + } + + /** + * Mounts an OBB file + * + * @param obbFilePath The full path to the OBB file to mount + * @param key (optional) The key to use to unencrypt the OBB; pass null for no encryption + * @param expectedState The expected state resulting from trying to mount the OBB + * @return A {@link String} representing the normalized path to OBB file that was mounted + */ + protected String mountObb(String obbFilePath, String key, int expectedState) { + return doMountObb(obbFilePath, key, expectedState); + } + + /** + * Mounts an OBB file with default options (no encryption, mounting succeeds) + * + * @param obbFilePath The full path to the OBB file to mount + * @return A {@link String} representing the normalized path to OBB file that was mounted + */ + protected String mountObb(String obbFilePath) { + return doMountObb(obbFilePath, null, OnObbStateChangeListener.MOUNTED); + } + + /** + * Synchronously waits for an OBB listener to be signaled of a state change, but does not throw + * + * @param obbListener The listener for the OBB file + * @return true if the listener was signaled of a state change by the system, else returns + * false if we time out. + */ + protected boolean doWaitForObbStateChange(ObbListener obbListener) { + synchronized(obbListener) { + long waitTimeMillis = 0; + while (!obbListener.isDone()) { + try { + Log.i(LOG_TAG, "Waiting for listener..."); + obbListener.wait(WAIT_TIME_INCR); + Log.i(LOG_TAG, "Awoke from waiting for listener..."); + waitTimeMillis += WAIT_TIME_INCR; + if (waitTimeMillis > MAX_WAIT_TIME) { + fail("Timed out waiting for OBB state to change!"); + } + } catch (InterruptedException e) { + Log.i(LOG_TAG, e.toString()); + } + } + return obbListener.isDone(); + } + } + + /** + * Synchronously waits for an OBB listener to be signaled of a state change + * + * @param obbListener The listener for the OBB file + * @return true if the listener was signaled of a state change by the system; else a fail() + * is triggered if we timed out + */ + protected String doMountObb_noThrow(String obbFilePath, String key, int expectedState) { + Log.i(LOG_TAG, "doMountObb() on " + obbFilePath + " using key: " + key); + assertTrue ("Null path was passed in for OBB file!", obbFilePath != null); + assertTrue ("Null path was passed in for OBB file!", obbFilePath != null); + + ObbListener obbListener = new ObbListener(); + boolean success = mSm.mountObb(obbFilePath, key, obbListener); + success &= obbFilePath.equals(doWaitForObbStateChange(obbListener)); + success &= (expectedState == obbListener.state()); + + if (OnObbStateChangeListener.MOUNTED == expectedState) { + success &= obbFilePath.equals(obbListener.officialPath()); + success &= mSm.isObbMounted(obbListener.officialPath()); + } else { + success &= !mSm.isObbMounted(obbListener.officialPath()); + } + + if (success) { + return obbListener.officialPath(); + } else { + return null; + } + } + + /** + * Mounts an OBB file without throwing and synchronously waits for it to finish mounting + * + * @param obbFilePath The full path to the OBB file to mount + * @param key (optional) The key to use to unencrypt the OBB; pass null for no encryption + * @param expectedState The expected state resulting from trying to mount the OBB + * @return A {@link String} representing the actual normalized path to OBB file that was + * mounted, or null if the mounting failed + */ + protected String doMountObb(String obbFilePath, String key, int expectedState) { + Log.i(LOG_TAG, "doMountObb() on " + obbFilePath + " using key: " + key); + assertTrue ("Null path was passed in for OBB file!", obbFilePath != null); + + ObbListener obbListener = new ObbListener(); + assertTrue("mountObb call failed", mSm.mountObb(obbFilePath, key, obbListener)); + assertTrue("Failed to get OBB mount status change for file: " + obbFilePath, + doWaitForObbStateChange(obbListener)); + assertEquals("OBB mount state not what was expected!", expectedState, obbListener.state()); + + if (OnObbStateChangeListener.MOUNTED == expectedState) { + assertEquals(obbFilePath, obbListener.officialPath()); + assertTrue("Obb should be mounted, but SM reports it is not!", + mSm.isObbMounted(obbListener.officialPath())); + } else if (OnObbStateChangeListener.UNMOUNTED == expectedState) { + assertFalse("Obb should not be mounted, but SM reports it is!", + mSm.isObbMounted(obbListener.officialPath())); + } + + assertEquals("Mount state is not what was expected!", expectedState, obbListener.state()); + return obbListener.officialPath(); + } + + /** + * Unmounts an OBB file without throwing, and synchronously waits for it to finish unmounting + * + * @param obbFilePath The full path to the OBB file to mount + * @param force true if we shuold force the unmount, false otherwise + * @return true if the unmount was successful, false otherwise + */ + protected boolean unmountObb_noThrow(String obbFilePath, boolean force) { + Log.i(LOG_TAG, "doUnmountObb_noThrow() on " + obbFilePath); + assertTrue ("Null path was passed in for OBB file!", obbFilePath != null); + boolean success = true; + + ObbListener obbListener = new ObbListener(); + assertTrue("unmountObb call failed", mSm.unmountObb(obbFilePath, force, obbListener)); + + boolean stateChanged = doWaitForObbStateChange(obbListener); + if (force) { + success &= stateChanged; + success &= (OnObbStateChangeListener.UNMOUNTED == obbListener.state()); + success &= !mSm.isObbMounted(obbFilePath); + } + return success; + } + + /** + * Unmounts an OBB file and synchronously waits for it to finish unmounting + * + * @param obbFilePath The full path to the OBB file to mount + * @param force true if we shuold force the unmount, false otherwise + */ + protected void unmountObb(String obbFilePath, boolean force) { + Log.i(LOG_TAG, "doUnmountObb() on " + obbFilePath); + assertTrue ("Null path was passed in for OBB file!", obbFilePath != null); + + ObbListener obbListener = new ObbListener(); + assertTrue("unmountObb call failed", mSm.unmountObb(obbFilePath, force, obbListener)); + + boolean stateChanged = doWaitForObbStateChange(obbListener); + if (force) { + assertTrue("Timed out waiting to unmount OBB file " + obbFilePath, stateChanged); + assertEquals("OBB failed to unmount", OnObbStateChangeListener.UNMOUNTED, + obbListener.state()); + assertFalse("Obb should NOT be mounted, but SM reports it is!", mSm.isObbMounted( + obbFilePath)); + } + } + + /** + * Helper to validate the contents of an "int" file in an OBB. + * + * The format of the files are sequential int's, in the range of: [start..end) + * + * @param path The full path to the file (path to OBB) + * @param filename The filename containing the ints to validate + * @param start The first int expected to be found in the file + * @param end The last int + 1 expected to be found in the file + */ + protected void doValidateIntContents(String path, String filename, int start, int end) { + File inFile = new File(path, filename); + DataInputStream inStream = null; + Log.i(LOG_TAG, "Validating file " + filename + " at " + path); + try { + inStream = new DataInputStream(new FileInputStream(inFile)); + + for (int i = start; i < end; ++i) { + if (inStream.readInt() != i) { + fail("Unexpected value read in OBB file"); + } + } + if (inStream != null) { + inStream.close(); + } + Log.i(LOG_TAG, "Successfully validated file " + filename); + } catch (FileNotFoundException e) { + fail("File " + inFile + " not found: " + e.toString()); + } catch (IOException e) { + fail("IOError with file " + inFile + ":" + e.toString()); + } + } + + /** + * Helper to validate the contents of a text file in an OBB + * + * @param path The full path to the file (path to OBB) + * @param filename The filename containing the ints to validate + * @param contents A {@link String} containing the expected contents of the file + */ + protected void doValidateTextContents(String path, String filename, String contents) { + File inFile = new File(path, filename); + BufferedReader fileReader = null; + BufferedReader textReader = null; + Log.i(LOG_TAG, "Validating file " + filename + " at " + path); + try { + fileReader = new BufferedReader(new FileReader(inFile)); + textReader = new BufferedReader(new StringReader(contents)); + String actual = null; + String expected = null; + while ((actual = fileReader.readLine()) != null) { + expected = textReader.readLine(); + if (!actual.equals(expected)) { + fail("File " + filename + " in OBB " + path + " does not match expected value"); + } + } + fileReader.close(); + textReader.close(); + Log.i(LOG_TAG, "File " + filename + " successfully verified."); + } catch (IOException e) { + fail("IOError with file " + inFile + ":" + e.toString()); + } + } + + /** + * Helper to validate the contents of a "long" file on our OBBs + * + * The format of the files are sequential 0's of type long + * + * @param path The full path to the file (path to OBB) + * @param filename The filename containing the ints to validate + * @param size The number of zero's expected in the file + * @param checkContents If true, the contents of the file are actually verified; if false, + * we simply verify that the file can be opened + */ + protected void doValidateZeroLongFile(String path, String filename, long size, + boolean checkContents) { + File inFile = new File(path, filename); + DataInputStream inStream = null; + Log.i(LOG_TAG, "Validating file " + filename + " at " + path); + try { + inStream = new DataInputStream(new FileInputStream(inFile)); + + if (checkContents) { + for (long i = 0; i < size; ++i) { + if (inStream.readLong() != 0) { + fail("Unexpected value read in OBB file" + filename); + } + } + } + + if (inStream != null) { + inStream.close(); + } + Log.i(LOG_TAG, "File " + filename + " successfully verified for " + size + " zeros"); + } catch (IOException e) { + fail("IOError with file " + inFile + ":" + e.toString()); + } + } + + /** + * Helper to synchronously wait until we can get a path for a given OBB file + * + * @param filePath The full normalized path to the OBB file + * @return The mounted path of the OBB, used to access contents in it + */ + protected String doWaitForPath(String filePath) { + String path = null; + + long waitTimeMillis = 0; + assertTrue("OBB " + filePath + " is not currently mounted!", mSm.isObbMounted(filePath)); + while (path == null) { + try { + Thread.sleep(WAIT_TIME_INCR); + waitTimeMillis += WAIT_TIME_INCR; + if (waitTimeMillis > MAX_WAIT_TIME) { + fail("Timed out waiting to get path of OBB file " + filePath); + } + } catch (InterruptedException e) { + // do nothing + } + path = mSm.getMountedObbPath(filePath); + } + Log.i(LOG_TAG, "Got OBB path: " + path); + return path; + } + + /** + * Verifies the pre-defined contents of our first OBB (OBB_FILE_1) + * + * The OBB contains 4 files and no subdirectories + * + * @param filePath The normalized path to the already-mounted OBB file + */ + protected void verifyObb1Contents(String filePath) { + String path = null; + path = doWaitForPath(filePath); + + // Validate contents of 2 files in this obb + doValidateIntContents(path, "OneToOneThousandInts.bin", 0, 1000); + doValidateIntContents(path, "SevenHundredInts.bin", 0, 700); + doValidateZeroLongFile(path, "FiveLongs.bin", 5, true); + } + + /** + * Verifies the pre-defined contents of our second OBB (OBB_FILE_2) + * + * The OBB contains 2 files and no subdirectories + * + * @param filePath The normalized path to the already-mounted OBB file + */ + protected void verifyObb2Contents(String filename) { + String path = null; + path = doWaitForPath(filename); + + // Validate contents of file + doValidateTextContents(path, "sample.txt", SAMPLE1_TEXT); + doValidateTextContents(path, "sample2.txt", SAMPLE2_TEXT); + } + + /** + * Verifies the pre-defined contents of our third OBB (OBB_FILE_3) + * + * The OBB contains nested files and subdirectories + * + * @param filePath The normalized path to the already-mounted OBB file + */ + protected void verifyObb3Contents(String filename) { + String path = null; + path = doWaitForPath(filename); + + // Validate contents of file + doValidateIntContents(path, "OneToOneThousandInts.bin", 0, 1000); + doValidateZeroLongFile(path, "TwoHundredLongs", 200, true); + + // validate subdirectory 1 + doValidateZeroLongFile(path + File.separator + "subdir1", "FiftyLongs", 50, true); + + // validate subdirectory subdir2/ + doValidateIntContents(path + File.separator + "subdir2", "OneToOneThousandInts", 0, 1000); + + // validate subdirectory subdir2/subdir2a/ + doValidateZeroLongFile(path + File.separator + "subdir2" + File.separator + "subdir2a", + "TwoHundredLongs", 200, true); + + // validate subdirectory subdir2/subdir2a/subdir2a1/ + doValidateIntContents(path + File.separator + "subdir2" + File.separator + "subdir2a" + + File.separator + "subdir2a1", "OneToOneThousandInts", 0, 1000); + } +} \ No newline at end of file diff --git a/src/main/java/android/os/storage/StorageManagerIntegrationTest.java b/src/main/java/android/os/storage/StorageManagerIntegrationTest.java new file mode 100644 index 0000000..71772d9 --- /dev/null +++ b/src/main/java/android/os/storage/StorageManagerIntegrationTest.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2010 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 android.os.storage; + +import android.content.Context; +import android.os.Environment; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.util.Log; + +import com.android.frameworks.coretests.R; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.File; +import java.io.FileInputStream; + +import junit.framework.AssertionFailedError; + +public class StorageManagerIntegrationTest extends StorageManagerBaseTest { + + private static String LOG_TAG = "StorageManagerBaseTest.StorageManagerIntegrationTest"; + protected File mFile = null; + + /** + * {@inheritDoc} + */ + @Override + public void setUp() throws Exception { + super.setUp(); + mContext = getInstrumentation().getContext(); + mFile = null; + } + + /** + * {@inheritDoc} + */ + @Override + protected void tearDown() throws Exception { + if (mFile != null) { + mFile.delete(); + mFile = null; + } + super.tearDown(); + } + + /** + * Tests mounting a single OBB file and verifies its contents. + */ + @LargeTest + public void testMountSingleObb() { + mFile = createObbFile(OBB_FILE_1, R.raw.obb_file1); + String filePath = mFile.getAbsolutePath(); + mountObb(filePath); + verifyObb1Contents(filePath); + unmountObb(filePath, DONT_FORCE); + } + + /** + * Tests mounting several OBB files and verifies its contents. + */ + @LargeTest + public void testMountMultipleObb() { + File file1 = null; + File file2 = null; + File file3 = null; + try { + file1 = createObbFile(OBB_FILE_1, R.raw.obb_file1); + String filePath1 = file1.getAbsolutePath(); + mountObb(filePath1); + verifyObb1Contents(filePath1); + + file2 = createObbFile(OBB_FILE_2, R.raw.obb_file2); + String filePath2 = file2.getAbsolutePath(); + mountObb(filePath2); + verifyObb2Contents(filePath2); + + file3 = createObbFile(OBB_FILE_3, R.raw.obb_file3); + String filePath3 = file3.getAbsolutePath(); + mountObb(filePath3); + verifyObb3Contents(filePath3); + + unmountObb(filePath1, DONT_FORCE); + unmountObb(filePath2, DONT_FORCE); + unmountObb(filePath3, DONT_FORCE); + } finally { + if (file1 != null) { + file1.delete(); + } + if (file2 != null) { + file2.delete(); + } + if (file3 != null) { + file3.delete(); + } + } + } + + /** + * Tests mounting a single encrypted OBB file and verifies its contents. + */ + @LargeTest + public void testMountSingleEncryptedObb() { + mFile = createObbFile(OBB_FILE_3_ENCRYPTED, R.raw.obb_enc_file100_orig3); + String filePath = mFile.getAbsolutePath(); + mountObb(filePath, OBB_FILE_3_PASSWORD, OnObbStateChangeListener.MOUNTED); + verifyObb3Contents(filePath); + unmountObb(filePath, DONT_FORCE); + } + + /** + * Tests mounting a single encrypted OBB file using an invalid password. + */ + @LargeTest + public void testMountSingleEncryptedObbInvalidPassword() { + mFile = createObbFile("bad password@$%#@^*(!&)", R.raw.obb_enc_file100_orig3); + String filePath = mFile.getAbsolutePath(); + mountObb(filePath, OBB_FILE_3_PASSWORD, OnObbStateChangeListener.ERROR_COULD_NOT_MOUNT); + unmountObb(filePath, DONT_FORCE); + } + + /** + * Tests simultaneously mounting 2 encrypted OBBs with different keys and verifies contents. + */ + @LargeTest + public void testMountTwoEncryptedObb() { + File file3 = null; + File file1 = null; + try { + file3 = createObbFile(OBB_FILE_3_ENCRYPTED, R.raw.obb_enc_file100_orig3); + String filePath3 = file3.getAbsolutePath(); + mountObb(filePath3, OBB_FILE_3_PASSWORD, OnObbStateChangeListener.MOUNTED); + verifyObb3Contents(filePath3); + + file1 = createObbFile(OBB_FILE_1_ENCRYPTED, R.raw.obb_enc_file100_orig1); + String filePath1 = file1.getAbsolutePath(); + mountObb(filePath1, OBB_FILE_1_PASSWORD, OnObbStateChangeListener.MOUNTED); + verifyObb1Contents(filePath1); + + unmountObb(filePath3, DONT_FORCE); + unmountObb(filePath1, DONT_FORCE); + } finally { + if (file3 != null) { + file3.delete(); + } + if (file1 != null) { + file1.delete(); + } + } + } + + /** + * Tests that we can not force unmount when a file is currently open on the OBB. + */ + @LargeTest + public void testUnmount_DontForce() { + mFile = createObbFile(OBB_FILE_1, R.raw.obb_file1); + String obbFilePath = mFile.getAbsolutePath(); + + MountingObbThread mountingThread = new MountingObbThread(obbFilePath, + OBB_FILE_1_CONTENTS_1); + + try { + mountingThread.start(); + + long waitTime = 0; + while (!mountingThread.isFileOpenOnObb()) { + synchronized (mountingThread) { + Log.i(LOG_TAG, "Waiting for file to be opened on OBB..."); + mountingThread.wait(WAIT_TIME_INCR); + waitTime += WAIT_TIME_INCR; + if (waitTime > MAX_WAIT_TIME) { + fail("Timed out waiting for file file to be opened on OBB!"); + } + } + } + + unmountObb(obbFilePath, DONT_FORCE); + + // verify still mounted + assertTrue("mounted path should not be null!", obbFilePath != null); + assertTrue("mounted path should still be mounted!", mSm.isObbMounted(obbFilePath)); + + // close the opened file + mountingThread.doStop(); + + // try unmounting again (should succeed this time) + unmountObb(obbFilePath, DONT_FORCE); + assertFalse("mounted path should no longer be mounted!", + mSm.isObbMounted(obbFilePath)); + } catch (InterruptedException e) { + fail("Timed out waiting for file on OBB to be opened..."); + } + } + + /** + * Tests mounting a single OBB that isn't signed. + */ + @LargeTest + public void testMountUnsignedObb() { + mFile = createObbFile(OBB_FILE_2_UNSIGNED, R.raw.obb_file2_nosign); + String filePath = mFile.getAbsolutePath(); + mountObb(filePath, OBB_FILE_2_UNSIGNED, OnObbStateChangeListener.ERROR_INTERNAL); + } + + /** + * Tests mounting a single OBB that is signed with a different package. + */ + @LargeTest + public void testMountBadPackageNameObb() { + mFile = createObbFile(OBB_FILE_3_BAD_PACKAGENAME, R.raw.obb_file3_bad_packagename); + String filePath = mFile.getAbsolutePath(); + mountObb(filePath, OBB_FILE_3_BAD_PACKAGENAME, + OnObbStateChangeListener.ERROR_PERMISSION_DENIED); + } + + /** + * Tests remounting a single OBB that has already been mounted. + */ + @LargeTest + public void testRemountObb() { + mFile = createObbFile(OBB_FILE_1, R.raw.obb_file1); + String filePath = mFile.getAbsolutePath(); + mountObb(filePath); + verifyObb1Contents(filePath); + mountObb(filePath, null, OnObbStateChangeListener.ERROR_ALREADY_MOUNTED); + verifyObb1Contents(filePath); + unmountObb(filePath, DONT_FORCE); + } +} \ No newline at end of file diff --git a/src/main/java/android/os/storage/StorageResultCode.java b/src/main/java/android/os/storage/StorageResultCode.java new file mode 100644 index 0000000..8e7db31 --- /dev/null +++ b/src/main/java/android/os/storage/StorageResultCode.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2007 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 android.os.storage; + +/** + * Class that provides access to constants returned from StorageManager + * and lower level MountService APIs. + * + * @hide + */ +public class StorageResultCode +{ + /** + * Operation succeeded. + * @see android.os.storage.StorageManager + */ + public static final int OperationSucceeded = 0; + + /** + * Operation failed: Internal error. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedInternalError = -1; + + /** + * Operation failed: Missing media. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedNoMedia = -2; + + /** + * Operation failed: Media is blank. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedMediaBlank = -3; + + /** + * Operation failed: Media is corrupt. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedMediaCorrupt = -4; + + /** + * Operation failed: Storage not mounted. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedStorageNotMounted = -5; + + /** + * Operation failed: Storage is mounted. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedStorageMounted = -6; + + /** + * Operation failed: Storage is busy. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedStorageBusy = -7; + +} diff --git a/src/main/java/android/os/storage/StorageVolume.java b/src/main/java/android/os/storage/StorageVolume.java new file mode 100644 index 0000000..06565f1 --- /dev/null +++ b/src/main/java/android/os/storage/StorageVolume.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2011 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 android.os.storage; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.UserHandle; + +import com.android.internal.util.IndentingPrintWriter; + +import java.io.CharArrayWriter; +import java.io.File; + +/** + * Description of a storage volume and its capabilities, including the + * filesystem path where it may be mounted. + * + * @hide + */ +public class StorageVolume implements Parcelable { + + // TODO: switch to more durable token + private int mStorageId; + + private final File mPath; + private final int mDescriptionId; + private final boolean mPrimary; + private final boolean mRemovable; + private final boolean mEmulated; + private final int mMtpReserveSpace; + private final boolean mAllowMassStorage; + /** Maximum file size for the storage, or zero for no limit */ + private final long mMaxFileSize; + /** When set, indicates exclusive ownership of this volume */ + private final UserHandle mOwner; + + private String mUuid; + private String mUserLabel; + private String mState; + + // StorageVolume extra for ACTION_MEDIA_REMOVED, ACTION_MEDIA_UNMOUNTED, ACTION_MEDIA_CHECKING, + // ACTION_MEDIA_NOFS, ACTION_MEDIA_MOUNTED, ACTION_MEDIA_SHARED, ACTION_MEDIA_UNSHARED, + // ACTION_MEDIA_BAD_REMOVAL, ACTION_MEDIA_UNMOUNTABLE and ACTION_MEDIA_EJECT broadcasts. + public static final String EXTRA_STORAGE_VOLUME = "storage_volume"; + + public StorageVolume(File path, int descriptionId, boolean primary, boolean removable, + boolean emulated, int mtpReserveSpace, boolean allowMassStorage, long maxFileSize, + UserHandle owner) { + mPath = path; + mDescriptionId = descriptionId; + mPrimary = primary; + mRemovable = removable; + mEmulated = emulated; + mMtpReserveSpace = mtpReserveSpace; + mAllowMassStorage = allowMassStorage; + mMaxFileSize = maxFileSize; + mOwner = owner; + } + + private StorageVolume(Parcel in) { + mStorageId = in.readInt(); + mPath = new File(in.readString()); + mDescriptionId = in.readInt(); + mPrimary = in.readInt() != 0; + mRemovable = in.readInt() != 0; + mEmulated = in.readInt() != 0; + mMtpReserveSpace = in.readInt(); + mAllowMassStorage = in.readInt() != 0; + mMaxFileSize = in.readLong(); + mOwner = in.readParcelable(null); + mUuid = in.readString(); + mUserLabel = in.readString(); + mState = in.readString(); + } + + public static StorageVolume fromTemplate(StorageVolume template, File path, UserHandle owner) { + return new StorageVolume(path, template.mDescriptionId, template.mPrimary, + template.mRemovable, template.mEmulated, template.mMtpReserveSpace, + template.mAllowMassStorage, template.mMaxFileSize, owner); + } + + /** + * Returns the mount path for the volume. + * + * @return the mount path + */ + public String getPath() { + return mPath.toString(); + } + + public File getPathFile() { + return mPath; + } + + /** + * Returns a user visible description of the volume. + * + * @return the volume description + */ + public String getDescription(Context context) { + return context.getResources().getString(mDescriptionId); + } + + public int getDescriptionId() { + return mDescriptionId; + } + + public boolean isPrimary() { + return mPrimary; + } + + /** + * Returns true if the volume is removable. + * + * @return is removable + */ + public boolean isRemovable() { + return mRemovable; + } + + /** + * Returns true if the volume is emulated. + * + * @return is removable + */ + public boolean isEmulated() { + return mEmulated; + } + + /** + * Returns the MTP storage ID for the volume. + * this is also used for the storage_id column in the media provider. + * + * @return MTP storage ID + */ + public int getStorageId() { + return mStorageId; + } + + /** + * Do not call this unless you are MountService + */ + public void setStorageId(int index) { + // storage ID is 0x00010001 for primary storage, + // then 0x00020001, 0x00030001, etc. for secondary storages + mStorageId = ((index + 1) << 16) + 1; + } + + /** + * Number of megabytes of space to leave unallocated by MTP. + * MTP will subtract this value from the free space it reports back + * to the host via GetStorageInfo, and will not allow new files to + * be added via MTP if there is less than this amount left free in the storage. + * If MTP has dedicated storage this value should be zero, but if MTP is + * sharing storage with the rest of the system, set this to a positive value + * to ensure that MTP activity does not result in the storage being + * too close to full. + * + * @return MTP reserve space + */ + public int getMtpReserveSpace() { + return mMtpReserveSpace; + } + + /** + * Returns true if this volume can be shared via USB mass storage. + * + * @return whether mass storage is allowed + */ + public boolean allowMassStorage() { + return mAllowMassStorage; + } + + /** + * Returns maximum file size for the volume, or zero if it is unbounded. + * + * @return maximum file size + */ + public long getMaxFileSize() { + return mMaxFileSize; + } + + public UserHandle getOwner() { + return mOwner; + } + + public void setUuid(String uuid) { + mUuid = uuid; + } + + public String getUuid() { + return mUuid; + } + + /** + * Parse and return volume UUID as FAT volume ID, or return -1 if unable to + * parse or UUID is unknown. + */ + public int getFatVolumeId() { + if (mUuid == null || mUuid.length() != 9) { + return -1; + } + try { + return (int)Long.parseLong(mUuid.replace("-", ""), 16); + } catch (NumberFormatException e) { + return -1; + } + } + + public void setUserLabel(String userLabel) { + mUserLabel = userLabel; + } + + public String getUserLabel() { + return mUserLabel; + } + + public void setState(String state) { + mState = state; + } + + public String getState() { + return mState; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StorageVolume && mPath != null) { + StorageVolume volume = (StorageVolume)obj; + return (mPath.equals(volume.mPath)); + } + return false; + } + + @Override + public int hashCode() { + return mPath.hashCode(); + } + + @Override + public String toString() { + final CharArrayWriter writer = new CharArrayWriter(); + dump(new IndentingPrintWriter(writer, " ", 80)); + return writer.toString(); + } + + public void dump(IndentingPrintWriter pw) { + pw.println("StorageVolume:"); + pw.increaseIndent(); + pw.printPair("mStorageId", mStorageId); + pw.printPair("mPath", mPath); + pw.printPair("mDescriptionId", mDescriptionId); + pw.printPair("mPrimary", mPrimary); + pw.printPair("mRemovable", mRemovable); + pw.printPair("mEmulated", mEmulated); + pw.printPair("mMtpReserveSpace", mMtpReserveSpace); + pw.printPair("mAllowMassStorage", mAllowMassStorage); + pw.printPair("mMaxFileSize", mMaxFileSize); + pw.printPair("mOwner", mOwner); + pw.printPair("mUuid", mUuid); + pw.printPair("mUserLabel", mUserLabel); + pw.printPair("mState", mState); + pw.decreaseIndent(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public StorageVolume createFromParcel(Parcel in) { + return new StorageVolume(in); + } + + @Override + public StorageVolume[] newArray(int size) { + return new StorageVolume[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mStorageId); + parcel.writeString(mPath.toString()); + parcel.writeInt(mDescriptionId); + parcel.writeInt(mPrimary ? 1 : 0); + parcel.writeInt(mRemovable ? 1 : 0); + parcel.writeInt(mEmulated ? 1 : 0); + parcel.writeInt(mMtpReserveSpace); + parcel.writeInt(mAllowMassStorage ? 1 : 0); + parcel.writeLong(mMaxFileSize); + parcel.writeParcelable(mOwner, flags); + parcel.writeString(mUuid); + parcel.writeString(mUserLabel); + parcel.writeString(mState); + } +} diff --git a/src/main/java/android/util/AndroidException.java b/src/main/java/android/util/AndroidException.java new file mode 100644 index 0000000..dfe00c9 --- /dev/null +++ b/src/main/java/android/util/AndroidException.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2006 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 android.util; + +/** + * Base class for all checked exceptions thrown by the Android frameworks. + */ +public class AndroidException extends Exception { + public AndroidException() { + } + + public AndroidException(String name) { + super(name); + } + + public AndroidException(String name, Throwable cause) { + super(name, cause); + } + + public AndroidException(Exception cause) { + super(cause); + } +}; + diff --git a/src/main/java/android/util/AndroidRuntimeException.java b/src/main/java/android/util/AndroidRuntimeException.java new file mode 100644 index 0000000..2b824bf --- /dev/null +++ b/src/main/java/android/util/AndroidRuntimeException.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2006 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 android.util; + +/** + * Base class for all unchecked exceptions thrown by the Android frameworks. + */ +public class AndroidRuntimeException extends RuntimeException { + public AndroidRuntimeException() { + } + + public AndroidRuntimeException(String name) { + super(name); + } + + public AndroidRuntimeException(String name, Throwable cause) { + super(name, cause); + } + + public AndroidRuntimeException(Exception cause) { + super(cause); + } +}; + diff --git a/src/main/java/android/util/ArrayMap.java b/src/main/java/android/util/ArrayMap.java new file mode 100644 index 0000000..6ed3885 --- /dev/null +++ b/src/main/java/android/util/ArrayMap.java @@ -0,0 +1,882 @@ +/* + * 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 android.util; + +import libcore.util.EmptyArray; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * ArrayMap is a generic key->value mapping data structure that is + * designed to be more memory efficient than a traditional {@link java.util.HashMap}. + * It keeps its mappings in an array data structure -- an integer array of hash + * codes for each item, and an Object array of the key/value pairs. This allows it to + * avoid having to create an extra object for every entry put in to the map, and it + * also tries to control the growth of the size of these arrays more aggressively + * (since growing them only requires copying the entries in the array, not rebuilding + * a hash map). + * + *

    Note that this implementation is not intended to be appropriate for data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    Because this container is intended to better balance memory use, unlike most other + * standard Java containers it will shrink its array as items are removed from it. Currently + * you have no control over this shrinking -- if you set a capacity and then remove an + * item, it may reduce the capacity to better match the current size. In the future an + * explicit call to set the capacity should turn off this aggressive shrinking behavior.

    + */ +public final class ArrayMap implements Map { + private static final boolean DEBUG = false; + private static final String TAG = "ArrayMap"; + + /** + * The minimum amount by which the capacity of a ArrayMap will increase. + * This is tuned to be relatively space-efficient. + */ + private static final int BASE_SIZE = 4; + + /** + * Maximum number of entries to have in array caches. + */ + private static final int CACHE_SIZE = 10; + + /** + * @hide Special immutable empty ArrayMap. + */ + public static final ArrayMap EMPTY = new ArrayMap(true); + + /** + * Caches of small array objects to avoid spamming garbage. The cache + * Object[] variable is a pointer to a linked list of array objects. + * The first entry in the array is a pointer to the next array in the + * list; the second entry is a pointer to the int[] hash code array for it. + */ + static Object[] mBaseCache; + static int mBaseCacheSize; + static Object[] mTwiceBaseCache; + static int mTwiceBaseCacheSize; + + /** + * Special hash array value that indicates the container is immutable. + */ + static final int[] EMPTY_IMMUTABLE_INTS = new int[0]; + + int[] mHashes; + Object[] mArray; + int mSize; + MapCollections mCollections; + + int indexOf(Object key, int hash) { + final int N = mSize; + + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + + int index = ContainerHelpers.binarySearch(mHashes, N, hash); + + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + + // If the key at the returned index matches, that's what we want. + if (key.equals(mArray[index<<1])) { + return index; + } + + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == hash; end++) { + if (key.equals(mArray[end << 1])) return end; + } + + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) { + if (key.equals(mArray[i << 1])) return i; + } + + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + int indexOfNull() { + final int N = mSize; + + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + + int index = ContainerHelpers.binarySearch(mHashes, N, 0); + + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + + // If the key at the returned index matches, that's what we want. + if (null == mArray[index<<1]) { + return index; + } + + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == 0; end++) { + if (null == mArray[end << 1]) return end; + } + + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) { + if (null == mArray[i << 1]) return i; + } + + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + private void allocArrays(final int size) { + if (mHashes == EMPTY_IMMUTABLE_INTS) { + throw new UnsupportedOperationException("ArrayMap is immutable"); + } + if (size == (BASE_SIZE*2)) { + synchronized (ArrayMap.class) { + if (mTwiceBaseCache != null) { + final Object[] array = mTwiceBaseCache; + mArray = array; + mTwiceBaseCache = (Object[])array[0]; + mHashes = (int[])array[1]; + array[0] = array[1] = null; + mTwiceBaseCacheSize--; + if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes + + " now have " + mTwiceBaseCacheSize + " entries"); + return; + } + } + } else if (size == BASE_SIZE) { + synchronized (ArrayMap.class) { + if (mBaseCache != null) { + final Object[] array = mBaseCache; + mArray = array; + mBaseCache = (Object[])array[0]; + mHashes = (int[])array[1]; + array[0] = array[1] = null; + mBaseCacheSize--; + if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes + + " now have " + mBaseCacheSize + " entries"); + return; + } + } + } + + mHashes = new int[size]; + mArray = new Object[size<<1]; + } + + private static void freeArrays(final int[] hashes, final Object[] array, final int size) { + if (hashes.length == (BASE_SIZE*2)) { + synchronized (ArrayMap.class) { + if (mTwiceBaseCacheSize < CACHE_SIZE) { + array[0] = mTwiceBaseCache; + array[1] = hashes; + for (int i=(size<<1)-1; i>=2; i--) { + array[i] = null; + } + mTwiceBaseCache = array; + mTwiceBaseCacheSize++; + if (DEBUG) Log.d(TAG, "Storing 2x cache " + array + + " now have " + mTwiceBaseCacheSize + " entries"); + } + } + } else if (hashes.length == BASE_SIZE) { + synchronized (ArrayMap.class) { + if (mBaseCacheSize < CACHE_SIZE) { + array[0] = mBaseCache; + array[1] = hashes; + for (int i=(size<<1)-1; i>=2; i--) { + array[i] = null; + } + mBaseCache = array; + mBaseCacheSize++; + if (DEBUG) Log.d(TAG, "Storing 1x cache " + array + + " now have " + mBaseCacheSize + " entries"); + } + } + } + } + + /** + * Create a new empty ArrayMap. The default capacity of an array map is 0, and + * will grow once items are added to it. + */ + public ArrayMap() { + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + mSize = 0; + } + + /** + * Create a new ArrayMap with a given initial capacity. + */ + public ArrayMap(int capacity) { + if (capacity == 0) { + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + } else { + allocArrays(capacity); + } + mSize = 0; + } + + private ArrayMap(boolean immutable) { + // If this is immutable, use the sentinal EMPTY_IMMUTABLE_INTS + // instance instead of the usual EmptyArray.INT. The reference + // is checked later to see if the array is allowed to grow. + mHashes = immutable ? EMPTY_IMMUTABLE_INTS : EmptyArray.INT; + mArray = EmptyArray.OBJECT; + mSize = 0; + } + + /** + * Create a new ArrayMap with the mappings from the given ArrayMap. + */ + public ArrayMap(ArrayMap map) { + this(); + if (map != null) { + putAll(map); + } + } + + /** + * Make the array map empty. All storage is released. + */ + @Override + public void clear() { + if (mSize > 0) { + freeArrays(mHashes, mArray, mSize); + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + mSize = 0; + } + } + + /** + * @hide + * Like {@link #clear}, but doesn't reduce the capacity of the ArrayMap. + */ + public void erase() { + if (mSize > 0) { + final int N = mSize<<1; + final Object[] array = mArray; + for (int i=0; iminimumCapacity + * items. + */ + public void ensureCapacity(int minimumCapacity) { + if (mHashes.length < minimumCapacity) { + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(minimumCapacity); + if (mSize > 0) { + System.arraycopy(ohashes, 0, mHashes, 0, mSize); + System.arraycopy(oarray, 0, mArray, 0, mSize<<1); + } + freeArrays(ohashes, oarray, mSize); + } + } + + /** + * Check whether a key exists in the array. + * + * @param key The key to search for. + * @return Returns true if the key exists, else false. + */ + @Override + public boolean containsKey(Object key) { + return indexOfKey(key) >= 0; + } + + /** + * Returns the index of a key in the set. + * + * @param key The key to search for. + * @return Returns the index of the key if it exists, else a negative integer. + */ + public int indexOfKey(Object key) { + return key == null ? indexOfNull() : indexOf(key, key.hashCode()); + } + + int indexOfValue(Object value) { + final int N = mSize*2; + final Object[] array = mArray; + if (value == null) { + for (int i=1; i>1; + } + } + } else { + for (int i=1; i>1; + } + } + } + return -1; + } + + /** + * Check whether a value exists in the array. This requires a linear search + * through the entire array. + * + * @param value The value to search for. + * @return Returns true if the value exists, else false. + */ + @Override + public boolean containsValue(Object value) { + return indexOfValue(value) >= 0; + } + + /** + * Retrieve a value from the array. + * @param key The key of the value to retrieve. + * @return Returns the value associated with the given key, + * or null if there is no such key. + */ + @Override + public V get(Object key) { + final int index = indexOfKey(key); + return index >= 0 ? (V)mArray[(index<<1)+1] : null; + } + + /** + * Return the key at the given index in the array. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the key stored at the given index. + */ + public K keyAt(int index) { + return (K)mArray[index << 1]; + } + + /** + * Return the value at the given index in the array. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value stored at the given index. + */ + public V valueAt(int index) { + return (V)mArray[(index << 1) + 1]; + } + + /** + * Set the value at a given index in the array. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @param value The new value to store at this index. + * @return Returns the previous value at the given index. + */ + public V setValueAt(int index, V value) { + index = (index << 1) + 1; + V old = (V)mArray[index]; + mArray[index] = value; + return old; + } + + /** + * Return true if the array map contains no items. + */ + @Override + public boolean isEmpty() { + return mSize <= 0; + } + + /** + * Add a new value to the array map. + * @param key The key under which to store the value. If + * this key already exists in the array, its value will be replaced. + * @param value The value to store for the given key. + * @return Returns the old value that was stored for the given key, or null if there + * was no such key. + */ + @Override + public V put(K key, V value) { + final int hash; + int index; + if (key == null) { + hash = 0; + index = indexOfNull(); + } else { + hash = key.hashCode(); + index = indexOf(key, hash); + } + if (index >= 0) { + index = (index<<1) + 1; + final V old = (V)mArray[index]; + mArray[index] = value; + return old; + } + + index = ~index; + if (mSize >= mHashes.length) { + final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1)) + : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE); + + if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n); + + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + + if (mHashes.length > 0) { + if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length); + System.arraycopy(oarray, 0, mArray, 0, oarray.length); + } + + freeArrays(ohashes, oarray, mSize); + } + + if (index < mSize) { + if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index) + + " to " + (index+1)); + System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index); + System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1); + } + + mHashes[index] = hash; + mArray[index<<1] = key; + mArray[(index<<1)+1] = value; + mSize++; + return null; + } + + /** + * Special fast path for appending items to the end of the array without validation. + * The array must already be large enough to contain the item. + * @hide + */ + public void append(K key, V value) { + int index = mSize; + final int hash = key == null ? 0 : key.hashCode(); + if (index >= mHashes.length) { + throw new IllegalStateException("Array is full"); + } + if (index > 0 && mHashes[index-1] > hash) { + RuntimeException e = new RuntimeException("here"); + e.fillInStackTrace(); + Log.w(TAG, "New hash " + hash + + " is before end of array hash " + mHashes[index-1] + + " at index " + index + " key " + key, e); + put(key, value); + return; + } + mSize = index+1; + mHashes[index] = hash; + index <<= 1; + mArray[index] = key; + mArray[index+1] = value; + } + + /** + * The use of the {@link #append} function can result in invalid array maps, in particular + * an array map where the same key appears multiple times. This function verifies that + * the array map is valid, throwing IllegalArgumentException if a problem is found. The + * main use for this method is validating an array map after unpacking from an IPC, to + * protect against malicious callers. + * @hide + */ + public void validate() { + final int N = mSize; + if (N <= 1) { + // There can't be dups. + return; + } + int basehash = mHashes[0]; + int basei = 0; + for (int i=1; i=basei; j--) { + final Object prev = mArray[j<<1]; + if (cur == prev) { + throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur); + } + if (cur != null && prev != null && cur.equals(prev)) { + throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur); + } + } + } + } + + /** + * Perform a {@link #put(Object, Object)} of all key/value pairs in array + * @param array The array whose contents are to be retrieved. + */ + public void putAll(ArrayMap array) { + final int N = array.mSize; + ensureCapacity(mSize + N); + if (mSize == 0) { + if (N > 0) { + System.arraycopy(array.mHashes, 0, mHashes, 0, N); + System.arraycopy(array.mArray, 0, mArray, 0, N<<1); + mSize = N; + } + } else { + for (int i=0; i= 0) { + return removeAt(index); + } + + return null; + } + + /** + * Remove the key/value mapping at the given index. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value that was stored at this index. + */ + public V removeAt(int index) { + final Object old = mArray[(index << 1) + 1]; + if (mSize <= 1) { + // Now empty. + if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0"); + freeArrays(mHashes, mArray, mSize); + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + mSize = 0; + } else { + if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) { + // Shrunk enough to reduce size of arrays. We don't allow it to + // shrink smaller than (BASE_SIZE*2) to avoid flapping between + // that and BASE_SIZE. + final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2); + + if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n); + + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + + mSize--; + if (index > 0) { + if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, index); + System.arraycopy(oarray, 0, mArray, 0, index << 1); + } + if (index < mSize) { + if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize + + " to " + index); + System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index); + System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1, + (mSize - index) << 1); + } + } else { + mSize--; + if (index < mSize) { + if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + mSize + + " to " + index); + System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index); + System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1, + (mSize - index) << 1); + } + mArray[mSize << 1] = null; + mArray[(mSize << 1) + 1] = null; + } + } + return (V)old; + } + + /** + * Return the number of items in this array map. + */ + @Override + public int size() { + return mSize; + } + + /** + * {@inheritDoc} + * + *

    This implementation returns false if the object is not a map, or + * if the maps have different sizes. Otherwise, for each key in this map, + * values of both maps are compared. If the values for any key are not + * equal, the method returns false, otherwise it returns true. + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object instanceof Map) { + Map map = (Map) object; + if (size() != map.size()) { + return false; + } + + try { + for (int i=0; iThis implementation composes a string by iterating over its mappings. If + * this map contains itself as a key or a value, the string "(this Map)" + * will appear in its place. + */ + @Override + public String toString() { + if (isEmpty()) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + Object key = keyAt(i); + if (key != this) { + buffer.append(key); + } else { + buffer.append("(this Map)"); + } + buffer.append('='); + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Map)"); + } + } + buffer.append('}'); + return buffer.toString(); + } + + // ------------------------------------------------------------------------ + // Interop with traditional Java containers. Not as efficient as using + // specialized collection APIs. + // ------------------------------------------------------------------------ + + private MapCollections getCollection() { + if (mCollections == null) { + mCollections = new MapCollections() { + @Override + protected int colGetSize() { + return mSize; + } + + @Override + protected Object colGetEntry(int index, int offset) { + return mArray[(index<<1) + offset]; + } + + @Override + protected int colIndexOfKey(Object key) { + return indexOfKey(key); + } + + @Override + protected int colIndexOfValue(Object value) { + return indexOfValue(value); + } + + @Override + protected Map colGetMap() { + return ArrayMap.this; + } + + @Override + protected void colPut(K key, V value) { + put(key, value); + } + + @Override + protected V colSetValue(int index, V value) { + return setValueAt(index, value); + } + + @Override + protected void colRemoveAt(int index) { + removeAt(index); + } + + @Override + protected void colClear() { + clear(); + } + }; + } + return mCollections; + } + + /** + * Determine if the array map contains all of the keys in the given collection. + * @param collection The collection whose contents are to be checked against. + * @return Returns true if this array map contains a key for every entry + * in collection, else returns false. + */ + public boolean containsAll(Collection collection) { + return MapCollections.containsAllHelper(this, collection); + } + + /** + * Perform a {@link #put(Object, Object)} of all key/value pairs in map + * @param map The map whose contents are to be retrieved. + */ + @Override + public void putAll(Map map) { + ensureCapacity(mSize + map.size()); + for (Map.Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /** + * Remove all keys in the array map that exist in the given collection. + * @param collection The collection whose contents are to be used to remove keys. + * @return Returns true if any keys were removed from the array map, else false. + */ + public boolean removeAll(Collection collection) { + return MapCollections.removeAllHelper(this, collection); + } + + /** + * Remove all keys in the array map that do not exist in the given collection. + * @param collection The collection whose contents are to be used to determine which + * keys to keep. + * @return Returns true if any keys were removed from the array map, else false. + */ + public boolean retainAll(Collection collection) { + return MapCollections.retainAllHelper(this, collection); + } + + /** + * Return a {@link java.util.Set} for iterating over and interacting with all mappings + * in the array map. + * + *

    Note: this is a very inefficient way to access the array contents, it + * requires generating a number of temporary objects.

    + * + *

    Note:

    the semantics of this + * Set are subtly different than that of a {@link java.util.HashMap}: most important, + * the {@link java.util.Map.Entry Map.Entry} object returned by its iterator is a single + * object that exists for the entire iterator, so you can not hold on to it + * after calling {@link java.util.Iterator#next() Iterator.next}.

    + */ + @Override + public Set> entrySet() { + return getCollection().getEntrySet(); + } + + /** + * Return a {@link java.util.Set} for iterating over and interacting with all keys + * in the array map. + * + *

    Note: this is a fairly inefficient way to access the array contents, it + * requires generating a number of temporary objects.

    + */ + @Override + public Set keySet() { + return getCollection().getKeySet(); + } + + /** + * Return a {@link java.util.Collection} for iterating over and interacting with all values + * in the array map. + * + *

    Note: this is a fairly inefficient way to access the array contents, it + * requires generating a number of temporary objects.

    + */ + @Override + public Collection values() { + return getCollection().getValues(); + } +} diff --git a/src/main/java/android/util/ArraySet.java b/src/main/java/android/util/ArraySet.java new file mode 100644 index 0000000..68f725e --- /dev/null +++ b/src/main/java/android/util/ArraySet.java @@ -0,0 +1,689 @@ +/* + * 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 android.util; + +import libcore.util.EmptyArray; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * ArraySet is a generic set data structure that is designed to be more memory efficient than a + * traditional {@link java.util.HashSet}. The design is very similar to + * {@link ArrayMap}, with all of the caveats described there. This implementation is + * separate from ArrayMap, however, so the Object array contains only one item for each + * entry in the set (instead of a pair for a mapping). + * + *

    Note that this implementation is not intended to be appropriate for data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashSet, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    Because this container is intended to better balance memory use, unlike most other + * standard Java containers it will shrink its array as items are removed from it. Currently + * you have no control over this shrinking -- if you set a capacity and then remove an + * item, it may reduce the capacity to better match the current size. In the future an + * explicit call to set the capacity should turn off this aggressive shrinking behavior.

    + * + * @hide + */ +public final class ArraySet implements Collection, Set { + private static final boolean DEBUG = false; + private static final String TAG = "ArraySet"; + + /** + * The minimum amount by which the capacity of a ArraySet will increase. + * This is tuned to be relatively space-efficient. + */ + private static final int BASE_SIZE = 4; + + /** + * Maximum number of entries to have in array caches. + */ + private static final int CACHE_SIZE = 10; + + /** + * Caches of small array objects to avoid spamming garbage. The cache + * Object[] variable is a pointer to a linked list of array objects. + * The first entry in the array is a pointer to the next array in the + * list; the second entry is a pointer to the int[] hash code array for it. + */ + static Object[] mBaseCache; + static int mBaseCacheSize; + static Object[] mTwiceBaseCache; + static int mTwiceBaseCacheSize; + + int[] mHashes; + Object[] mArray; + int mSize; + MapCollections mCollections; + + private int indexOf(Object key, int hash) { + final int N = mSize; + + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + + int index = ContainerHelpers.binarySearch(mHashes, N, hash); + + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + + // If the key at the returned index matches, that's what we want. + if (key.equals(mArray[index])) { + return index; + } + + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == hash; end++) { + if (key.equals(mArray[end])) return end; + } + + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) { + if (key.equals(mArray[i])) return i; + } + + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + private int indexOfNull() { + final int N = mSize; + + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + + int index = ContainerHelpers.binarySearch(mHashes, N, 0); + + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + + // If the key at the returned index matches, that's what we want. + if (null == mArray[index]) { + return index; + } + + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == 0; end++) { + if (null == mArray[end]) return end; + } + + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) { + if (null == mArray[i]) return i; + } + + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + private void allocArrays(final int size) { + if (size == (BASE_SIZE*2)) { + synchronized (ArraySet.class) { + if (mTwiceBaseCache != null) { + final Object[] array = mTwiceBaseCache; + mArray = array; + mTwiceBaseCache = (Object[])array[0]; + mHashes = (int[])array[1]; + array[0] = array[1] = null; + mTwiceBaseCacheSize--; + if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes + + " now have " + mTwiceBaseCacheSize + " entries"); + return; + } + } + } else if (size == BASE_SIZE) { + synchronized (ArraySet.class) { + if (mBaseCache != null) { + final Object[] array = mBaseCache; + mArray = array; + mBaseCache = (Object[])array[0]; + mHashes = (int[])array[1]; + array[0] = array[1] = null; + mBaseCacheSize--; + if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes + + " now have " + mBaseCacheSize + " entries"); + return; + } + } + } + + mHashes = new int[size]; + mArray = new Object[size]; + } + + private static void freeArrays(final int[] hashes, final Object[] array, final int size) { + if (hashes.length == (BASE_SIZE*2)) { + synchronized (ArraySet.class) { + if (mTwiceBaseCacheSize < CACHE_SIZE) { + array[0] = mTwiceBaseCache; + array[1] = hashes; + for (int i=size-1; i>=2; i--) { + array[i] = null; + } + mTwiceBaseCache = array; + mTwiceBaseCacheSize++; + if (DEBUG) Log.d(TAG, "Storing 2x cache " + array + + " now have " + mTwiceBaseCacheSize + " entries"); + } + } + } else if (hashes.length == BASE_SIZE) { + synchronized (ArraySet.class) { + if (mBaseCacheSize < CACHE_SIZE) { + array[0] = mBaseCache; + array[1] = hashes; + for (int i=size-1; i>=2; i--) { + array[i] = null; + } + mBaseCache = array; + mBaseCacheSize++; + if (DEBUG) Log.d(TAG, "Storing 1x cache " + array + + " now have " + mBaseCacheSize + " entries"); + } + } + } + } + + /** + * Create a new empty ArraySet. The default capacity of an array map is 0, and + * will grow once items are added to it. + */ + public ArraySet() { + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + mSize = 0; + } + + /** + * Create a new ArraySet with a given initial capacity. + */ + public ArraySet(int capacity) { + if (capacity == 0) { + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + } else { + allocArrays(capacity); + } + mSize = 0; + } + + /** + * Create a new ArraySet with the mappings from the given ArraySet. + */ + public ArraySet(ArraySet set) { + this(); + if (set != null) { + addAll(set); + } + } + + /** {@hide} */ + public ArraySet(Collection set) { + this(); + if (set != null) { + addAll(set); + } + } + + /** + * Make the array map empty. All storage is released. + */ + @Override + public void clear() { + if (mSize != 0) { + freeArrays(mHashes, mArray, mSize); + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + mSize = 0; + } + } + + /** + * Ensure the array map can hold at least minimumCapacity + * items. + */ + public void ensureCapacity(int minimumCapacity) { + if (mHashes.length < minimumCapacity) { + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(minimumCapacity); + if (mSize > 0) { + System.arraycopy(ohashes, 0, mHashes, 0, mSize); + System.arraycopy(oarray, 0, mArray, 0, mSize); + } + freeArrays(ohashes, oarray, mSize); + } + } + + /** + * Check whether a value exists in the set. + * + * @param key The value to search for. + * @return Returns true if the value exists, else false. + */ + @Override + public boolean contains(Object key) { + return indexOf(key) >= 0; + } + + /** + * Returns the index of a value in the set. + * + * @param key The value to search for. + * @return Returns the index of the value if it exists, else a negative integer. + */ + public int indexOf(Object key) { + return key == null ? indexOfNull() : indexOf(key, key.hashCode()); + } + + /** + * Return the value at the given index in the array. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value stored at the given index. + */ + public E valueAt(int index) { + return (E)mArray[index]; + } + + /** + * Return true if the array map contains no items. + */ + @Override + public boolean isEmpty() { + return mSize <= 0; + } + + /** + * Adds the specified object to this set. The set is not modified if it + * already contains the object. + * + * @param value the object to add. + * @return {@code true} if this set is modified, {@code false} otherwise. + * @throws ClassCastException + * when the class of the object is inappropriate for this set. + */ + @Override + public boolean add(E value) { + final int hash; + int index; + if (value == null) { + hash = 0; + index = indexOfNull(); + } else { + hash = value.hashCode(); + index = indexOf(value, hash); + } + if (index >= 0) { + return false; + } + + index = ~index; + if (mSize >= mHashes.length) { + final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1)) + : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE); + + if (DEBUG) Log.d(TAG, "add: grow from " + mHashes.length + " to " + n); + + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + + if (mHashes.length > 0) { + if (DEBUG) Log.d(TAG, "add: copy 0-" + mSize + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length); + System.arraycopy(oarray, 0, mArray, 0, oarray.length); + } + + freeArrays(ohashes, oarray, mSize); + } + + if (index < mSize) { + if (DEBUG) Log.d(TAG, "add: move " + index + "-" + (mSize-index) + + " to " + (index+1)); + System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index); + System.arraycopy(mArray, index, mArray, index + 1, mSize - index); + } + + mHashes[index] = hash; + mArray[index] = value; + mSize++; + return true; + } + + /** + * Perform a {@link #add(Object)} of all values in array + * @param array The array whose contents are to be retrieved. + */ + public void addAll(ArraySet array) { + final int N = array.mSize; + ensureCapacity(mSize + N); + if (mSize == 0) { + if (N > 0) { + System.arraycopy(array.mHashes, 0, mHashes, 0, N); + System.arraycopy(array.mArray, 0, mArray, 0, N); + mSize = N; + } + } else { + for (int i=0; i= 0) { + removeAt(index); + return true; + } + return false; + } + + /** + * Remove the key/value mapping at the given index. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value that was stored at this index. + */ + public E removeAt(int index) { + final Object old = mArray[index]; + if (mSize <= 1) { + // Now empty. + if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0"); + freeArrays(mHashes, mArray, mSize); + mHashes = EmptyArray.INT; + mArray = EmptyArray.OBJECT; + mSize = 0; + } else { + if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) { + // Shrunk enough to reduce size of arrays. We don't allow it to + // shrink smaller than (BASE_SIZE*2) to avoid flapping between + // that and BASE_SIZE. + final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2); + + if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n); + + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + + mSize--; + if (index > 0) { + if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, index); + System.arraycopy(oarray, 0, mArray, 0, index); + } + if (index < mSize) { + if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize + + " to " + index); + System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index); + System.arraycopy(oarray, index + 1, mArray, index, mSize - index); + } + } else { + mSize--; + if (index < mSize) { + if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + mSize + + " to " + index); + System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index); + System.arraycopy(mArray, index + 1, mArray, index, mSize - index); + } + mArray[mSize] = null; + } + } + return (E)old; + } + + /** + * Return the number of items in this array map. + */ + @Override + public int size() { + return mSize; + } + + @Override + public Object[] toArray() { + Object[] result = new Object[mSize]; + System.arraycopy(mArray, 0, result, 0, mSize); + return result; + } + + @Override + public T[] toArray(T[] array) { + if (array.length < mSize) { + @SuppressWarnings("unchecked") T[] newArray + = (T[]) Array.newInstance(array.getClass().getComponentType(), mSize); + array = newArray; + } + System.arraycopy(mArray, 0, array, 0, mSize); + if (array.length > mSize) { + array[mSize] = null; + } + return array; + } + + /** + * {@inheritDoc} + * + *

    This implementation returns false if the object is not a set, or + * if the sets have different sizes. Otherwise, for each value in this + * set, it checks to make sure the value also exists in the other set. + * If any value doesn't exist, the method returns false; otherwise, it + * returns true. + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object instanceof Set) { + Set set = (Set) object; + if (size() != set.size()) { + return false; + } + + try { + for (int i=0; iThis implementation composes a string by iterating over its values. If + * this set contains itself as a value, the string "(this Set)" + * will appear in its place. + */ + @Override + public String toString() { + if (isEmpty()) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 14); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Set)"); + } + } + buffer.append('}'); + return buffer.toString(); + } + + // ------------------------------------------------------------------------ + // Interop with traditional Java containers. Not as efficient as using + // specialized collection APIs. + // ------------------------------------------------------------------------ + + private MapCollections getCollection() { + if (mCollections == null) { + mCollections = new MapCollections() { + @Override + protected int colGetSize() { + return mSize; + } + + @Override + protected Object colGetEntry(int index, int offset) { + return mArray[index]; + } + + @Override + protected int colIndexOfKey(Object key) { + return indexOf(key); + } + + @Override + protected int colIndexOfValue(Object value) { + return indexOf(value); + } + + @Override + protected Map colGetMap() { + throw new UnsupportedOperationException("not a map"); + } + + @Override + protected void colPut(E key, E value) { + add(key); + } + + @Override + protected E colSetValue(int index, E value) { + throw new UnsupportedOperationException("not a map"); + } + + @Override + protected void colRemoveAt(int index) { + removeAt(index); + } + + @Override + protected void colClear() { + clear(); + } + }; + } + return mCollections; + } + + @Override + public Iterator iterator() { + return getCollection().getKeySet().iterator(); + } + + @Override + public boolean containsAll(Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!contains(it.next())) { + return false; + } + } + return true; + } + + @Override + public boolean addAll(Collection collection) { + ensureCapacity(mSize + collection.size()); + boolean added = false; + for (E value : collection) { + added |= add(value); + } + return added; + } + + @Override + public boolean removeAll(Collection collection) { + boolean removed = false; + for (Object value : collection) { + removed |= remove(value); + } + return removed; + } + + @Override + public boolean retainAll(Collection collection) { + boolean removed = false; + for (int i=mSize-1; i>=0; i--) { + if (!collection.contains(mArray[i])) { + removeAt(i); + removed = true; + } + } + return removed; + } +} diff --git a/src/main/java/android/util/AtomicFile.java b/src/main/java/android/util/AtomicFile.java new file mode 100644 index 0000000..a6466fc --- /dev/null +++ b/src/main/java/android/util/AtomicFile.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2009 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 android.util; + +import android.os.FileUtils; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Helper class for performing atomic operations on a file by creating a + * backup file until a write has successfully completed. If you need this + * on older versions of the platform you can use + * {@link android.support.v4.util.AtomicFile} in the v4 support library. + *

    + * Atomic file guarantees file integrity by ensuring that a file has + * been completely written and sync'd to disk before removing its backup. + * As long as the backup file exists, the original file is considered + * to be invalid (left over from a previous attempt to write the file). + *

    + * Atomic file does not confer any file locking semantics. + * Do not use this class when the file may be accessed or modified concurrently + * by multiple threads or processes. The caller is responsible for ensuring + * appropriate mutual exclusion invariants whenever it accesses the file. + *

    + */ +public class AtomicFile { + private final File mBaseName; + private final File mBackupName; + + /** + * Create a new AtomicFile for a file located at the given File path. + * The secondary backup file will be the same file path with ".bak" appended. + */ + public AtomicFile(File baseName) { + mBaseName = baseName; + mBackupName = new File(baseName.getPath() + ".bak"); + } + + /** + * Return the path to the base file. You should not generally use this, + * as the data at that path may not be valid. + */ + public File getBaseFile() { + return mBaseName; + } + + /** + * Delete the atomic file. This deletes both the base and backup files. + */ + public void delete() { + mBaseName.delete(); + mBackupName.delete(); + } + + /** + * Start a new write operation on the file. This returns a FileOutputStream + * to which you can write the new file data. The existing file is replaced + * with the new data. You must not directly close the given + * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} + * or {@link #failWrite(FileOutputStream)}. + * + *

    Note that if another thread is currently performing + * a write, this will simply replace whatever that thread is writing + * with the new file being written by this thread, and when the other + * thread finishes the write the new write operation will no longer be + * safe (or will be lost). You must do your own threading protection for + * access to AtomicFile. + */ + public FileOutputStream startWrite() throws IOException { + // Rename the current file so it may be used as a backup during the next read + if (mBaseName.exists()) { + if (!mBackupName.exists()) { + if (!mBaseName.renameTo(mBackupName)) { + Log.w("AtomicFile", "Couldn't rename file " + mBaseName + + " to backup file " + mBackupName); + } + } else { + mBaseName.delete(); + } + } + FileOutputStream str = null; + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e) { + File parent = mBaseName.getParentFile(); + if (!parent.mkdir()) { + throw new IOException("Couldn't create directory " + mBaseName); + } + FileUtils.setPermissions( + parent.getPath(), + FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, + -1, -1); + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e2) { + throw new IOException("Couldn't create " + mBaseName); + } + } + return str; + } + + /** + * Call when you have successfully finished writing to the stream + * returned by {@link #startWrite()}. This will close, sync, and + * commit the new data. The next attempt to read the atomic file + * will return the new file stream. + */ + public void finishWrite(FileOutputStream str) { + if (str != null) { + FileUtils.sync(str); + try { + str.close(); + mBackupName.delete(); + } catch (IOException e) { + Log.w("AtomicFile", "finishWrite: Got exception:", e); + } + } + } + + /** + * Call when you have failed for some reason at writing to the stream + * returned by {@link #startWrite()}. This will close the current + * write stream, and roll back to the previous state of the file. + */ + public void failWrite(FileOutputStream str) { + if (str != null) { + FileUtils.sync(str); + try { + str.close(); + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } catch (IOException e) { + Log.w("AtomicFile", "failWrite: Got exception:", e); + } + } + } + + /** @hide + * @deprecated This is not safe. + */ + @Deprecated public void truncate() throws IOException { + try { + FileOutputStream fos = new FileOutputStream(mBaseName); + FileUtils.sync(fos); + fos.close(); + } catch (FileNotFoundException e) { + throw new IOException("Couldn't append " + mBaseName); + } catch (IOException e) { + } + } + + /** @hide + * @deprecated This is not safe. + */ + @Deprecated public FileOutputStream openAppend() throws IOException { + try { + return new FileOutputStream(mBaseName, true); + } catch (FileNotFoundException e) { + throw new IOException("Couldn't append " + mBaseName); + } + } + + /** + * Open the atomic file for reading. If there previously was an + * incomplete write, this will roll back to the last good data before + * opening for read. You should call close() on the FileInputStream when + * you are done reading from it. + * + *

    Note that if another thread is currently performing + * a write, this will incorrectly consider it to be in the state of a bad + * write and roll back, causing the new data currently being written to + * be dropped. You must do your own threading protection for access to + * AtomicFile. + */ + public FileInputStream openRead() throws FileNotFoundException { + if (mBackupName.exists()) { + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } + return new FileInputStream(mBaseName); + } + + /** + * Gets the last modified time of the atomic file. + * {@hide} + * + * @return last modified time in milliseconds since epoch. + * @throws IOException + */ + public long getLastModifiedTime() throws IOException { + if (mBackupName.exists()) { + return mBackupName.lastModified(); + } + return mBaseName.lastModified(); + } + + /** + * A convenience for {@link #openRead()} that also reads all of the + * file contents into a byte array which is returned. + */ + public byte[] readFully() throws IOException { + FileInputStream stream = openRead(); + try { + int pos = 0; + int avail = stream.available(); + byte[] data = new byte[avail]; + while (true) { + int amt = stream.read(data, pos, data.length-pos); + //Log.i("foo", "Read " + amt + " bytes at " + pos + // + " of avail " + data.length); + if (amt <= 0) { + //Log.i("foo", "**** FINISHED READING: pos=" + pos + // + " len=" + data.length); + return data; + } + pos += amt; + avail = stream.available(); + if (avail > data.length-pos) { + byte[] newData = new byte[pos+avail]; + System.arraycopy(data, 0, newData, 0, pos); + data = newData; + } + } + } finally { + stream.close(); + } + } +} diff --git a/src/main/java/android/util/AttributeSet.java b/src/main/java/android/util/AttributeSet.java new file mode 100644 index 0000000..74942ba --- /dev/null +++ b/src/main/java/android/util/AttributeSet.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2006 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 android.util; + + +/** + * A collection of attributes, as found associated with a tag in an XML + * document. Often you will not want to use this interface directly, instead + * passing it to {@link android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int) + * Resources.Theme.obtainStyledAttributes()} + * which will take care of parsing the attributes for you. In particular, + * the Resources API will convert resource references (attribute values such as + * "@string/my_label" in the original XML) to the desired type + * for you; if you use AttributeSet directly then you will need to manually + * check for resource references + * (with {@link #getAttributeResourceValue(int, int)}) and do the resource + * lookup yourself if needed. Direct use of AttributeSet also prevents the + * application of themes and styles when retrieving attribute values. + * + *

    This interface provides an efficient mechanism for retrieving + * data from compiled XML files, which can be retrieved for a particular + * XmlPullParser through {@link Xml#asAttributeSet + * Xml.asAttributeSet()}. Normally this will return an implementation + * of the interface that works on top of a generic XmlPullParser, however it + * is more useful in conjunction with compiled XML resources: + * + *

    + * XmlPullParser parser = resources.getXml(myResouce);
    + * AttributeSet attributes = Xml.asAttributeSet(parser);
    + * + *

    The implementation returned here, unlike using + * the implementation on top of a generic XmlPullParser, + * is highly optimized by retrieving pre-computed information that was + * generated by aapt when compiling your resources. For example, + * the {@link #getAttributeFloatValue(int, float)} method returns a floating + * point number previous stored in the compiled resource instead of parsing + * at runtime the string originally in the XML file. + * + *

    This interface also provides additional information contained in the + * compiled XML resource that is not available in a normal XML file, such + * as {@link #getAttributeNameResource(int)} which returns the resource + * identifier associated with a particular XML attribute name. + */ +public interface AttributeSet { + /** + * Returns the number of attributes available in the set. + * + * @return A positive integer, or 0 if the set is empty. + */ + public int getAttributeCount(); + + /** + * Returns the name of the specified attribute. + * + * @param index Index of the desired attribute, 0...count-1. + * + * @return A String containing the name of the attribute, or null if the + * attribute cannot be found. + */ + public String getAttributeName(int index); + + /** + * Returns the value of the specified attribute as a string representation. + * + * @param index Index of the desired attribute, 0...count-1. + * + * @return A String containing the value of the attribute, or null if the + * attribute cannot be found. + */ + public String getAttributeValue(int index); + + /** + * Returns the value of the specified attribute as a string representation. + * The lookup is performed using the attribute name. + * + * @param namespace The namespace of the attribute to get the value from. + * @param name The name of the attribute to get the value from. + * + * @return A String containing the value of the attribute, or null if the + * attribute cannot be found. + */ + public String getAttributeValue(String namespace, String name); + + /** + * Returns a description of the current position of the attribute set. + * For instance, if the attribute set is loaded from an XML document, + * the position description could indicate the current line number. + * + * @return A string representation of the current position in the set, + * may be null. + */ + public String getPositionDescription(); + + /** + * Return the resource ID associated with the given attribute name. This + * will be the identifier for an attribute resource, which can be used by + * styles. Returns 0 if there is no resource associated with this + * attribute. + * + *

    Note that this is different than {@link #getAttributeResourceValue} + * in that it returns a resource identifier for the attribute name; the + * other method returns this attribute's value as a resource identifier. + * + * @param index Index of the desired attribute, 0...count-1. + * + * @return The resource identifier, 0 if none. + */ + public int getAttributeNameResource(int index); + + /** + * Return the index of the value of 'attribute' in the list 'options'. + * + * @param namespace Namespace of attribute to retrieve. + * @param attribute Name of attribute to retrieve. + * @param options List of strings whose values we are checking against. + * @param defaultValue Value returned if attribute doesn't exist or no + * match is found. + * + * @return Index in to 'options' or defaultValue. + */ + public int getAttributeListValue(String namespace, String attribute, + String[] options, int defaultValue); + + /** + * Return the boolean value of 'attribute'. + * + * @param namespace Namespace of attribute to retrieve. + * @param attribute The attribute to retrieve. + * @param defaultValue What to return if the attribute isn't found. + * + * @return Resulting value. + */ + public boolean getAttributeBooleanValue(String namespace, String attribute, + boolean defaultValue); + + /** + * Return the value of 'attribute' as a resource identifier. + * + *

    Note that this is different than {@link #getAttributeNameResource} + * in that it returns the value contained in this attribute as a + * resource identifier (i.e., a value originally of the form + * "@package:type/resource"); the other method returns a resource + * identifier that identifies the name of the attribute. + * + * @param namespace Namespace of attribute to retrieve. + * @param attribute The attribute to retrieve. + * @param defaultValue What to return if the attribute isn't found. + * + * @return Resulting value. + */ + public int getAttributeResourceValue(String namespace, String attribute, + int defaultValue); + + /** + * Return the integer value of 'attribute'. + * + * @param namespace Namespace of attribute to retrieve. + * @param attribute The attribute to retrieve. + * @param defaultValue What to return if the attribute isn't found. + * + * @return Resulting value. + */ + public int getAttributeIntValue(String namespace, String attribute, + int defaultValue); + + /** + * Return the boolean value of 'attribute' that is formatted as an + * unsigned value. In particular, the formats 0xn...n and #n...n are + * handled. + * + * @param namespace Namespace of attribute to retrieve. + * @param attribute The attribute to retrieve. + * @param defaultValue What to return if the attribute isn't found. + * + * @return Resulting value. + */ + public int getAttributeUnsignedIntValue(String namespace, String attribute, + int defaultValue); + + /** + * Return the float value of 'attribute'. + * + * @param namespace Namespace of attribute to retrieve. + * @param attribute The attribute to retrieve. + * @param defaultValue What to return if the attribute isn't found. + * + * @return Resulting value. + */ + public float getAttributeFloatValue(String namespace, String attribute, + float defaultValue); + + /** + * Return the index of the value of attribute at 'index' in the list + * 'options'. + * + * @param index Index of the desired attribute, 0...count-1. + * @param options List of strings whose values we are checking against. + * @param defaultValue Value returned if attribute doesn't exist or no + * match is found. + * + * @return Index in to 'options' or defaultValue. + */ + public int getAttributeListValue(int index, String[] options, int defaultValue); + + /** + * Return the boolean value of attribute at 'index'. + * + * @param index Index of the desired attribute, 0...count-1. + * @param defaultValue What to return if the attribute isn't found. + * + * @return Resulting value. + */ + public boolean getAttributeBooleanValue(int index, boolean defaultValue); + + /** + * Return the value of attribute at 'index' as a resource identifier. + * + *

    Note that this is different than {@link #getAttributeNameResource} + * in that it returns the value contained in this attribute as a + * resource identifier (i.e., a value originally of the form + * "@package:type/resource"); the other method returns a resource + * identifier that identifies the name of the attribute. + * + * @param index Index of the desired attribute, 0...count-1. + * @param defaultValue What to return if the attribute isn't found. + * + * @return Resulting value. + */ + public int getAttributeResourceValue(int index, int defaultValue); + + /** + * Return the integer value of attribute at 'index'. + * + * @param index Index of the desired attribute, 0...count-1. + * @param defaultValue What to return if the attribute isn't found. + * + * @return Resulting value. + */ + public int getAttributeIntValue(int index, int defaultValue); + + /** + * Return the integer value of attribute at 'index' that is formatted as an + * unsigned value. In particular, the formats 0xn...n and #n...n are + * handled. + * + * @param index Index of the desired attribute, 0...count-1. + * @param defaultValue What to return if the attribute isn't found. + * + * @return Resulting value. + */ + public int getAttributeUnsignedIntValue(int index, int defaultValue); + + /** + * Return the float value of attribute at 'index'. + * + * @param index Index of the desired attribute, 0...count-1. + * @param defaultValue What to return if the attribute isn't found. + * + * @return Resulting value. + */ + public float getAttributeFloatValue(int index, float defaultValue); + + /** + * Return the value of the "id" attribute or null if there is not one. + * Equivalent to getAttributeValue(null, "id"). + * + * @return The id attribute's value or null. + */ + public String getIdAttribute(); + + /** + * Return the value of the "class" attribute or null if there is not one. + * Equivalent to getAttributeValue(null, "class"). + * + * @return The class attribute's value or null. + */ + public String getClassAttribute(); + + /** + * Return the integer value of the "id" attribute or defaultValue if there + * is none. + * Equivalent to getAttributeResourceValue(null, "id", defaultValue); + * + * @param defaultValue What to return if the "id" attribute isn't found. + * @return int Resulting value. + */ + public int getIdAttributeResourceValue(int defaultValue); + + /** + + * Return the value of the "style" attribute or 0 if there is not one. + * Equivalent to getAttributeResourceValue(null, "style"). + * + * @return The style attribute's resource identifier or 0. + */ + public int getStyleAttribute(); +} diff --git a/src/main/java/android/util/Base64.java b/src/main/java/android/util/Base64.java new file mode 100644 index 0000000..1f2a5a7 --- /dev/null +++ b/src/main/java/android/util/Base64.java @@ -0,0 +1,741 @@ +/* + * Copyright (C) 2010 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 android.util; + +import java.io.UnsupportedEncodingException; + +/** + * Utilities for encoding and decoding the Base64 representation of + * binary data. See RFCs 2045 and 3548. + */ +public class Base64 { + /** + * Default values for encoder/decoder flags. + */ + public static final int DEFAULT = 0; + + /** + * Encoder flag bit to omit the padding '=' characters at the end + * of the output (if any). + */ + public static final int NO_PADDING = 1; + + /** + * Encoder flag bit to omit all line terminators (i.e., the output + * will be on one long line). + */ + public static final int NO_WRAP = 2; + + /** + * Encoder flag bit to indicate lines should be terminated with a + * CRLF pair instead of just an LF. Has no effect if {@code + * NO_WRAP} is specified as well. + */ + public static final int CRLF = 4; + + /** + * Encoder/decoder flag bit to indicate using the "URL and + * filename safe" variant of Base64 (see RFC 3548 section 4) where + * {@code -} and {@code _} are used in place of {@code +} and + * {@code /}. + */ + public static final int URL_SAFE = 8; + + /** + * Flag to pass to {@link Base64OutputStream} to indicate that it + * should not close the output stream it is wrapping when it + * itself is closed. + */ + public static final int NO_CLOSE = 16; + + // -------------------------------------------------------- + // shared code + // -------------------------------------------------------- + + /* package */ static abstract class Coder { + public byte[] output; + public int op; + + /** + * Encode/decode another block of input data. this.output is + * provided by the caller, and must be big enough to hold all + * the coded data. On exit, this.opwill be set to the length + * of the coded data. + * + * @param finish true if this is the final call to process for + * this object. Will finalize the coder state and + * include any final bytes in the output. + * + * @return true if the input so far is good; false if some + * error has been detected in the input stream.. + */ + public abstract boolean process(byte[] input, int offset, int len, boolean finish); + + /** + * @return the maximum number of bytes a call to process() + * could produce for the given number of input bytes. This may + * be an overestimate. + */ + public abstract int maxOutputSize(int len); + } + + // -------------------------------------------------------- + // decoding + // -------------------------------------------------------- + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + *

    The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param str the input String to decode, which is converted to + * bytes using the default charset + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(String str, int flags) { + return decode(str.getBytes(), flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + *

    The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the input array to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int flags) { + return decode(input, 0, input.length, flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + *

    The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the data to decode + * @param offset the position within the input array at which to start + * @param len the number of bytes of input to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int offset, int len, int flags) { + // Allocate space for the most data the input could represent. + // (It could contain less if it contains whitespace, etc.) + Decoder decoder = new Decoder(flags, new byte[len*3/4]); + + if (!decoder.process(input, offset, len, true)) { + throw new IllegalArgumentException("bad base-64"); + } + + // Maybe we got lucky and allocated exactly enough output space. + if (decoder.op == decoder.output.length) { + return decoder.output; + } + + // Need to shorten the array, so allocate a new one of the + // right size and copy. + byte[] temp = new byte[decoder.op]; + System.arraycopy(decoder.output, 0, temp, 0, decoder.op); + return temp; + } + + /* package */ static class Decoder extends Coder { + /** + * Lookup table for turning bytes into their position in the + * Base64 alphabet. + */ + private static final int DECODE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** + * Decode lookup table for the "web safe" variant (RFC 3548 + * sec. 4) where - and _ replace + and /. + */ + private static final int DECODE_WEBSAFE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** Non-data values in the DECODE arrays. */ + private static final int SKIP = -1; + private static final int EQUALS = -2; + + /** + * States 0-3 are reading through the next input tuple. + * State 4 is having read one '=' and expecting exactly + * one more. + * State 5 is expecting no more data or padding characters + * in the input. + * State 6 is the error state; an error has been detected + * in the input and no future input can "fix" it. + */ + private int state; // state number (0 to 6) + private int value; + + final private int[] alphabet; + + public Decoder(int flags, byte[] output) { + this.output = output; + + alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE; + state = 0; + value = 0; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could decode to. + */ + public int maxOutputSize(int len) { + return len * 3/4 + 10; + } + + /** + * Decode another block of input data. + * + * @return true if the state machine is still healthy. false if + * bad base-64 data has been detected in the input stream. + */ + public boolean process(byte[] input, int offset, int len, boolean finish) { + if (this.state == 6) return false; + + int p = offset; + len += offset; + + // Using local variables makes the decoder about 12% + // faster than if we manipulate the member variables in + // the loop. (Even alphabet makes a measurable + // difference, which is somewhat surprising to me since + // the member variable is final.) + int state = this.state; + int value = this.value; + int op = 0; + final byte[] output = this.output; + final int[] alphabet = this.alphabet; + + while (p < len) { + // Try the fast path: we're starting a new tuple and the + // next four bytes of the input stream are all data + // bytes. This corresponds to going through states + // 0-1-2-3-0. We expect to use this method for most of + // the data. + // + // If any of the next four bytes of input are non-data + // (whitespace, etc.), value will end up negative. (All + // the non-data values in decode are small negative + // numbers, so shifting any of them up and or'ing them + // together will result in a value with its top bit set.) + // + // You can remove this whole block and the output should + // be the same, just slower. + if (state == 0) { + while (p+4 <= len && + (value = ((alphabet[input[p] & 0xff] << 18) | + (alphabet[input[p+1] & 0xff] << 12) | + (alphabet[input[p+2] & 0xff] << 6) | + (alphabet[input[p+3] & 0xff]))) >= 0) { + output[op+2] = (byte) value; + output[op+1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + p += 4; + } + if (p >= len) break; + } + + // The fast path isn't available -- either we've read a + // partial tuple, or the next four input bytes aren't all + // data, or whatever. Fall back to the slower state + // machine implementation. + + int d = alphabet[input[p++] & 0xff]; + + switch (state) { + case 0: + if (d >= 0) { + value = d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 1: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 2: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect exactly one more padding character. + output[op++] = (byte) (value >> 4); + state = 4; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 3: + if (d >= 0) { + // Emit the output triple and return to state 0. + value = (value << 6) | d; + output[op+2] = (byte) value; + output[op+1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + state = 0; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect no further data or padding characters. + output[op+1] = (byte) (value >> 2); + output[op] = (byte) (value >> 10); + op += 2; + state = 5; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 4: + if (d == EQUALS) { + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 5: + if (d != SKIP) { + this.state = 6; + return false; + } + break; + } + } + + if (!finish) { + // We're out of input, but a future call could provide + // more. + this.state = state; + this.value = value; + this.op = op; + return true; + } + + // Done reading input. Now figure out where we are left in + // the state machine and finish up. + + switch (state) { + case 0: + // Output length is a multiple of three. Fine. + break; + case 1: + // Read one extra input byte, which isn't enough to + // make another output byte. Illegal. + this.state = 6; + return false; + case 2: + // Read two extra input bytes, enough to emit 1 more + // output byte. Fine. + output[op++] = (byte) (value >> 4); + break; + case 3: + // Read three extra input bytes, enough to emit 2 more + // output bytes. Fine. + output[op++] = (byte) (value >> 10); + output[op++] = (byte) (value >> 2); + break; + case 4: + // Read one padding '=' when we expected 2. Illegal. + this.state = 6; + return false; + case 5: + // Read all the padding '='s we expected and no more. + // Fine. + break; + } + + this.state = state; + this.op = op; + return true; + } + } + + // -------------------------------------------------------- + // encoding + // -------------------------------------------------------- + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int flags) { + try { + return new String(encode(input, flags), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + // US-ASCII is guaranteed to be available. + throw new AssertionError(e); + } + } + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int offset, int len, int flags) { + try { + return new String(encode(input, offset, len, flags), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + // US-ASCII is guaranteed to be available. + throw new AssertionError(e); + } + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int flags) { + return encode(input, 0, input.length, flags); + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int offset, int len, int flags) { + Encoder encoder = new Encoder(flags, null); + + // Compute the exact length of the array we will produce. + int output_len = len / 3 * 4; + + // Account for the tail of the data and the padding bytes, if any. + if (encoder.do_padding) { + if (len % 3 > 0) { + output_len += 4; + } + } else { + switch (len % 3) { + case 0: break; + case 1: output_len += 2; break; + case 2: output_len += 3; break; + } + } + + // Account for the newlines, if any. + if (encoder.do_newline && len > 0) { + output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) * + (encoder.do_cr ? 2 : 1); + } + + encoder.output = new byte[output_len]; + encoder.process(input, offset, len, true); + + assert encoder.op == output_len; + + return encoder.output; + } + + /* package */ static class Encoder extends Coder { + /** + * Emit a new line every this many output tuples. Corresponds to + * a 76-character line length (the maximum allowable according to + * RFC 2045). + */ + public static final int LINE_GROUPS = 19; + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte ENCODE[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', + }; + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte ENCODE_WEBSAFE[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', + }; + + final private byte[] tail; + /* package */ int tailLen; + private int count; + + final public boolean do_padding; + final public boolean do_newline; + final public boolean do_cr; + final private byte[] alphabet; + + public Encoder(int flags, byte[] output) { + this.output = output; + + do_padding = (flags & NO_PADDING) == 0; + do_newline = (flags & NO_WRAP) == 0; + do_cr = (flags & CRLF) != 0; + alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; + + tail = new byte[2]; + tailLen = 0; + + count = do_newline ? LINE_GROUPS : -1; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could encode to. + */ + public int maxOutputSize(int len) { + return len * 8/5 + 10; + } + + public boolean process(byte[] input, int offset, int len, boolean finish) { + // Using local variables makes the encoder about 9% faster. + final byte[] alphabet = this.alphabet; + final byte[] output = this.output; + int op = 0; + int count = this.count; + + int p = offset; + len += offset; + int v = -1; + + // First we need to concatenate the tail of the previous call + // with any input bytes available now and see if we can empty + // the tail. + + switch (tailLen) { + case 0: + // There was no tail. + break; + + case 1: + if (p+2 <= len) { + // A 1-byte tail with at least 2 bytes of + // input available now. + v = ((tail[0] & 0xff) << 16) | + ((input[p++] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + }; + break; + + case 2: + if (p+1 <= len) { + // A 2-byte tail with at least 1 byte of input. + v = ((tail[0] & 0xff) << 16) | + ((tail[1] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + } + break; + } + + if (v != -1) { + output[op++] = alphabet[(v >> 18) & 0x3f]; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + + // At this point either there is no tail, or there are fewer + // than 3 bytes of input available. + + // The main loop, turning 3 input bytes into 4 output bytes on + // each iteration. + while (p+3 <= len) { + v = ((input[p] & 0xff) << 16) | + ((input[p+1] & 0xff) << 8) | + (input[p+2] & 0xff); + output[op] = alphabet[(v >> 18) & 0x3f]; + output[op+1] = alphabet[(v >> 12) & 0x3f]; + output[op+2] = alphabet[(v >> 6) & 0x3f]; + output[op+3] = alphabet[v & 0x3f]; + p += 3; + op += 4; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + + if (finish) { + // Finish up the tail of the input. Note that we need to + // consume any bytes in tail before any bytes + // remaining in input; there should be at most two bytes + // total. + + if (p-tailLen == len-1) { + int t = 0; + v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4; + tailLen -= t; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (p-tailLen == len-2) { + int t = 0; + v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) | + (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2); + tailLen -= t; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (do_newline && op > 0 && count != LINE_GROUPS) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + + assert tailLen == 0; + assert p == len; + } else { + // Save the leftovers in tail to be consumed on the next + // call to encodeInternal. + + if (p == len-1) { + tail[tailLen++] = input[p]; + } else if (p == len-2) { + tail[tailLen++] = input[p]; + tail[tailLen++] = input[p+1]; + } + } + + this.op = op; + this.count = count; + + return true; + } + } + + private Base64() { } // don't instantiate +} diff --git a/src/main/java/android/util/Base64DataException.java b/src/main/java/android/util/Base64DataException.java new file mode 100644 index 0000000..de12ee1 --- /dev/null +++ b/src/main/java/android/util/Base64DataException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 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 android.util; + +import java.io.IOException; + +/** + * This exception is thrown by {@link Base64InputStream} or {@link Base64OutputStream} + * when an error is detected in the data being decoded. This allows problems with the base64 data + * to be disambiguated from errors in the underlying streams (e.g. actual connection errors.) + */ +public class Base64DataException extends IOException { + public Base64DataException(String detailMessage) { + super(detailMessage); + } +} diff --git a/src/main/java/android/util/Base64InputStream.java b/src/main/java/android/util/Base64InputStream.java new file mode 100644 index 0000000..9eba5b5 --- /dev/null +++ b/src/main/java/android/util/Base64InputStream.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2010 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 android.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * An InputStream that does Base64 decoding on the data read through + * it. + */ +public class Base64InputStream extends FilterInputStream { + private final Base64.Coder coder; + + private static byte[] EMPTY = new byte[0]; + + private static final int BUFFER_SIZE = 2048; + private boolean eof; + private byte[] inputBuffer; + private int outputStart; + private int outputEnd; + + /** + * An InputStream that performs Base64 decoding on the data read + * from the wrapped stream. + * + * @param in the InputStream to read the source data from + * @param flags bit flags for controlling the decoder; see the + * constants in {@link Base64} + */ + public Base64InputStream(InputStream in, int flags) { + this(in, flags, false); + } + + /** + * Performs Base64 encoding or decoding on the data read from the + * wrapped InputStream. + * + * @param in the InputStream to read the source data from + * @param flags bit flags for controlling the decoder; see the + * constants in {@link Base64} + * @param encode true to encode, false to decode + * + * @hide + */ + public Base64InputStream(InputStream in, int flags, boolean encode) { + super(in); + eof = false; + inputBuffer = new byte[BUFFER_SIZE]; + if (encode) { + coder = new Base64.Encoder(flags, null); + } else { + coder = new Base64.Decoder(flags, null); + } + coder.output = new byte[coder.maxOutputSize(BUFFER_SIZE)]; + outputStart = 0; + outputEnd = 0; + } + + public boolean markSupported() { + return false; + } + + public void mark(int readlimit) { + throw new UnsupportedOperationException(); + } + + public void reset() { + throw new UnsupportedOperationException(); + } + + public void close() throws IOException { + in.close(); + inputBuffer = null; + } + + public int available() { + return outputEnd - outputStart; + } + + public long skip(long n) throws IOException { + if (outputStart >= outputEnd) { + refill(); + } + if (outputStart >= outputEnd) { + return 0; + } + long bytes = Math.min(n, outputEnd-outputStart); + outputStart += bytes; + return bytes; + } + + public int read() throws IOException { + if (outputStart >= outputEnd) { + refill(); + } + if (outputStart >= outputEnd) { + return -1; + } else { + return coder.output[outputStart++] & 0xff; + } + } + + public int read(byte[] b, int off, int len) throws IOException { + if (outputStart >= outputEnd) { + refill(); + } + if (outputStart >= outputEnd) { + return -1; + } + int bytes = Math.min(len, outputEnd-outputStart); + System.arraycopy(coder.output, outputStart, b, off, bytes); + outputStart += bytes; + return bytes; + } + + /** + * Read data from the input stream into inputBuffer, then + * decode/encode it into the empty coder.output, and reset the + * outputStart and outputEnd pointers. + */ + private void refill() throws IOException { + if (eof) return; + int bytesRead = in.read(inputBuffer); + boolean success; + if (bytesRead == -1) { + eof = true; + success = coder.process(EMPTY, 0, 0, true); + } else { + success = coder.process(inputBuffer, 0, bytesRead, false); + } + if (!success) { + throw new Base64DataException("bad base-64"); + } + outputEnd = coder.op; + outputStart = 0; + } +} diff --git a/src/main/java/android/util/Base64OutputStream.java b/src/main/java/android/util/Base64OutputStream.java new file mode 100644 index 0000000..4535d1c --- /dev/null +++ b/src/main/java/android/util/Base64OutputStream.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2010 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 android.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * An OutputStream that does Base64 encoding on the data written to + * it, writing the resulting data to another OutputStream. + */ +public class Base64OutputStream extends FilterOutputStream { + private final Base64.Coder coder; + private final int flags; + + private byte[] buffer = null; + private int bpos = 0; + + private static byte[] EMPTY = new byte[0]; + + /** + * Performs Base64 encoding on the data written to the stream, + * writing the encoded data to another OutputStream. + * + * @param out the OutputStream to write the encoded data to + * @param flags bit flags for controlling the encoder; see the + * constants in {@link Base64} + */ + public Base64OutputStream(OutputStream out, int flags) { + this(out, flags, true); + } + + /** + * Performs Base64 encoding or decoding on the data written to the + * stream, writing the encoded/decoded data to another + * OutputStream. + * + * @param out the OutputStream to write the encoded data to + * @param flags bit flags for controlling the encoder; see the + * constants in {@link Base64} + * @param encode true to encode, false to decode + * + * @hide + */ + public Base64OutputStream(OutputStream out, int flags, boolean encode) { + super(out); + this.flags = flags; + if (encode) { + coder = new Base64.Encoder(flags, null); + } else { + coder = new Base64.Decoder(flags, null); + } + } + + public void write(int b) throws IOException { + // To avoid invoking the encoder/decoder routines for single + // bytes, we buffer up calls to write(int) in an internal + // byte array to transform them into writes of decently-sized + // arrays. + + if (buffer == null) { + buffer = new byte[1024]; + } + if (bpos >= buffer.length) { + // internal buffer full; write it out. + internalWrite(buffer, 0, bpos, false); + bpos = 0; + } + buffer[bpos++] = (byte) b; + } + + /** + * Flush any buffered data from calls to write(int). Needed + * before doing a write(byte[], int, int) or a close(). + */ + private void flushBuffer() throws IOException { + if (bpos > 0) { + internalWrite(buffer, 0, bpos, false); + bpos = 0; + } + } + + public void write(byte[] b, int off, int len) throws IOException { + if (len <= 0) return; + flushBuffer(); + internalWrite(b, off, len, false); + } + + public void close() throws IOException { + IOException thrown = null; + try { + flushBuffer(); + internalWrite(EMPTY, 0, 0, true); + } catch (IOException e) { + thrown = e; + } + + try { + if ((flags & Base64.NO_CLOSE) == 0) { + out.close(); + } else { + out.flush(); + } + } catch (IOException e) { + if (thrown != null) { + thrown = e; + } + } + + if (thrown != null) { + throw thrown; + } + } + + /** + * Write the given bytes to the encoder/decoder. + * + * @param finish true if this is the last batch of input, to cause + * encoder/decoder state to be finalized. + */ + private void internalWrite(byte[] b, int off, int len, boolean finish) throws IOException { + coder.output = embiggen(coder.output, coder.maxOutputSize(len)); + if (!coder.process(b, off, len, finish)) { + throw new Base64DataException("bad base-64"); + } + out.write(coder.output, 0, coder.op); + } + + /** + * If b.length is at least len, return b. Otherwise return a new + * byte array of length len. + */ + private byte[] embiggen(byte[] b, int len) { + if (b == null || b.length < len) { + return new byte[len]; + } else { + return b; + } + } +} diff --git a/src/main/java/android/util/Base64Test.java b/src/main/java/android/util/Base64Test.java new file mode 100644 index 0000000..53368d4 --- /dev/null +++ b/src/main/java/android/util/Base64Test.java @@ -0,0 +1,531 @@ +/* + * Copyright (C) 2010 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 android.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Random; + +public class Base64Test extends TestCase { + private static final String TAG = "Base64Test"; + + /** Decodes a string, returning a string. */ + private String decodeString(String in) throws Exception { + byte[] out = Base64.decode(in, 0); + return new String(out); + } + + /** + * Encodes the string 'in' using 'flags'. Asserts that decoding + * gives the same string. Returns the encoded string. + */ + private String encodeToString(String in, int flags) throws Exception { + String b64 = Base64.encodeToString(in.getBytes(), flags); + String dec = decodeString(b64); + assertEquals(in, dec); + return b64; + } + + /** Assert that decoding 'in' throws IllegalArgumentException. */ + private void assertBad(String in) throws Exception { + try { + byte[] out = Base64.decode(in, 0); + fail("should have failed to decode"); + } catch (IllegalArgumentException e) { + } + } + + /** Assert that actual equals the first len bytes of expected. */ + private void assertEquals(byte[] expected, int len, byte[] actual) { + assertEquals(len, actual.length); + for (int i = 0; i < len; ++i) { + assertEquals(expected[i], actual[i]); + } + } + + /** Assert that actual equals the first len bytes of expected. */ + private void assertEquals(byte[] expected, int len, byte[] actual, int alen) { + assertEquals(len, alen); + for (int i = 0; i < len; ++i) { + assertEquals(expected[i], actual[i]); + } + } + + /** Assert that actual equals the first len bytes of expected. */ + private void assertEquals(byte[] expected, byte[] actual) { + assertEquals(expected.length, actual.length); + for (int i = 0; i < expected.length; ++i) { + assertEquals(expected[i], actual[i]); + } + } + + public void testDecodeExtraChars() throws Exception { + // padding 0 + assertEquals("hello, world", decodeString("aGVsbG8sIHdvcmxk")); + assertBad("aGVsbG8sIHdvcmxk="); + assertBad("aGVsbG8sIHdvcmxk=="); + assertBad("aGVsbG8sIHdvcmxk ="); + assertBad("aGVsbG8sIHdvcmxk = = "); + assertEquals("hello, world", decodeString(" aGVs bG8s IHdv cmxk ")); + assertEquals("hello, world", decodeString(" aGV sbG8 sIHd vcmx k ")); + assertEquals("hello, world", decodeString(" aG VsbG 8sIH dvcm xk ")); + assertEquals("hello, world", decodeString(" a GVsb G8sI Hdvc mxk ")); + assertEquals("hello, world", decodeString(" a G V s b G 8 s I H d v c m x k ")); + assertEquals("hello, world", decodeString("_a*G_V*s_b*G_8*s_I*H_d*v_c*m_x*k_")); + assertEquals("hello, world", decodeString("aGVsbG8sIHdvcmxk")); + + // padding 1 + assertEquals("hello, world?!", decodeString("aGVsbG8sIHdvcmxkPyE=")); + assertEquals("hello, world?!", decodeString("aGVsbG8sIHdvcmxkPyE")); + assertBad("aGVsbG8sIHdvcmxkPyE=="); + assertBad("aGVsbG8sIHdvcmxkPyE =="); + assertBad("aGVsbG8sIHdvcmxkPyE = = "); + assertEquals("hello, world?!", decodeString("aGVsbG8sIHdvcmxkPy E=")); + assertEquals("hello, world?!", decodeString("aGVsbG8sIHdvcmxkPy E")); + assertEquals("hello, world?!", decodeString("aGVsbG8sIHdvcmxkPy E =")); + assertEquals("hello, world?!", decodeString("aGVsbG8sIHdvcmxkPy E ")); + assertEquals("hello, world?!", decodeString("aGVsbG8sIHdvcmxkPy E = ")); + assertEquals("hello, world?!", decodeString("aGVsbG8sIHdvcmxkPy E ")); + + // padding 2 + assertEquals("hello, world.", decodeString("aGVsbG8sIHdvcmxkLg==")); + assertEquals("hello, world.", decodeString("aGVsbG8sIHdvcmxkLg")); + assertBad("aGVsbG8sIHdvcmxkLg="); + assertBad("aGVsbG8sIHdvcmxkLg ="); + assertBad("aGVsbG8sIHdvcmxkLg = "); + assertEquals("hello, world.", decodeString("aGVsbG8sIHdvcmxkL g==")); + assertEquals("hello, world.", decodeString("aGVsbG8sIHdvcmxkL g")); + assertEquals("hello, world.", decodeString("aGVsbG8sIHdvcmxkL g ==")); + assertEquals("hello, world.", decodeString("aGVsbG8sIHdvcmxkL g ")); + assertEquals("hello, world.", decodeString("aGVsbG8sIHdvcmxkL g = = ")); + assertEquals("hello, world.", decodeString("aGVsbG8sIHdvcmxkL g ")); + } + + private static final byte[] BYTES = { (byte) 0xff, (byte) 0xee, (byte) 0xdd, + (byte) 0xcc, (byte) 0xbb, (byte) 0xaa, + (byte) 0x99, (byte) 0x88, (byte) 0x77 }; + + public void testBinaryDecode() throws Exception { + assertEquals(BYTES, 0, Base64.decode("", 0)); + assertEquals(BYTES, 1, Base64.decode("/w==", 0)); + assertEquals(BYTES, 2, Base64.decode("/+4=", 0)); + assertEquals(BYTES, 3, Base64.decode("/+7d", 0)); + assertEquals(BYTES, 4, Base64.decode("/+7dzA==", 0)); + assertEquals(BYTES, 5, Base64.decode("/+7dzLs=", 0)); + assertEquals(BYTES, 6, Base64.decode("/+7dzLuq", 0)); + assertEquals(BYTES, 7, Base64.decode("/+7dzLuqmQ==", 0)); + assertEquals(BYTES, 8, Base64.decode("/+7dzLuqmYg=", 0)); + } + + public void testWebSafe() throws Exception { + assertEquals(BYTES, 0, Base64.decode("", Base64.URL_SAFE)); + assertEquals(BYTES, 1, Base64.decode("_w==", Base64.URL_SAFE)); + assertEquals(BYTES, 2, Base64.decode("_-4=", Base64.URL_SAFE)); + assertEquals(BYTES, 3, Base64.decode("_-7d", Base64.URL_SAFE)); + assertEquals(BYTES, 4, Base64.decode("_-7dzA==", Base64.URL_SAFE)); + assertEquals(BYTES, 5, Base64.decode("_-7dzLs=", Base64.URL_SAFE)); + assertEquals(BYTES, 6, Base64.decode("_-7dzLuq", Base64.URL_SAFE)); + assertEquals(BYTES, 7, Base64.decode("_-7dzLuqmQ==", Base64.URL_SAFE)); + assertEquals(BYTES, 8, Base64.decode("_-7dzLuqmYg=", Base64.URL_SAFE)); + + assertEquals("", Base64.encodeToString(BYTES, 0, 0, Base64.URL_SAFE)); + assertEquals("_w==\n", Base64.encodeToString(BYTES, 0, 1, Base64.URL_SAFE)); + assertEquals("_-4=\n", Base64.encodeToString(BYTES, 0, 2, Base64.URL_SAFE)); + assertEquals("_-7d\n", Base64.encodeToString(BYTES, 0, 3, Base64.URL_SAFE)); + assertEquals("_-7dzA==\n", Base64.encodeToString(BYTES, 0, 4, Base64.URL_SAFE)); + assertEquals("_-7dzLs=\n", Base64.encodeToString(BYTES, 0, 5, Base64.URL_SAFE)); + assertEquals("_-7dzLuq\n", Base64.encodeToString(BYTES, 0, 6, Base64.URL_SAFE)); + assertEquals("_-7dzLuqmQ==\n", Base64.encodeToString(BYTES, 0, 7, Base64.URL_SAFE)); + assertEquals("_-7dzLuqmYg=\n", Base64.encodeToString(BYTES, 0, 8, Base64.URL_SAFE)); + } + + public void testFlags() throws Exception { + assertEquals("YQ==\n", encodeToString("a", 0)); + assertEquals("YQ==", encodeToString("a", Base64.NO_WRAP)); + assertEquals("YQ\n", encodeToString("a", Base64.NO_PADDING)); + assertEquals("YQ", encodeToString("a", Base64.NO_PADDING | Base64.NO_WRAP)); + assertEquals("YQ==\r\n", encodeToString("a", Base64.CRLF)); + assertEquals("YQ\r\n", encodeToString("a", Base64.CRLF | Base64.NO_PADDING)); + + assertEquals("YWI=\n", encodeToString("ab", 0)); + assertEquals("YWI=", encodeToString("ab", Base64.NO_WRAP)); + assertEquals("YWI\n", encodeToString("ab", Base64.NO_PADDING)); + assertEquals("YWI", encodeToString("ab", Base64.NO_PADDING | Base64.NO_WRAP)); + assertEquals("YWI=\r\n", encodeToString("ab", Base64.CRLF)); + assertEquals("YWI\r\n", encodeToString("ab", Base64.CRLF | Base64.NO_PADDING)); + + assertEquals("YWJj\n", encodeToString("abc", 0)); + assertEquals("YWJj", encodeToString("abc", Base64.NO_WRAP)); + assertEquals("YWJj\n", encodeToString("abc", Base64.NO_PADDING)); + assertEquals("YWJj", encodeToString("abc", Base64.NO_PADDING | Base64.NO_WRAP)); + assertEquals("YWJj\r\n", encodeToString("abc", Base64.CRLF)); + assertEquals("YWJj\r\n", encodeToString("abc", Base64.CRLF | Base64.NO_PADDING)); + + assertEquals("YWJjZA==\n", encodeToString("abcd", 0)); + assertEquals("YWJjZA==", encodeToString("abcd", Base64.NO_WRAP)); + assertEquals("YWJjZA\n", encodeToString("abcd", Base64.NO_PADDING)); + assertEquals("YWJjZA", encodeToString("abcd", Base64.NO_PADDING | Base64.NO_WRAP)); + assertEquals("YWJjZA==\r\n", encodeToString("abcd", Base64.CRLF)); + assertEquals("YWJjZA\r\n", encodeToString("abcd", Base64.CRLF | Base64.NO_PADDING)); + } + + public void testLineLength() throws Exception { + String in_56 = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd"; + String in_57 = in_56 + "e"; + String in_58 = in_56 + "ef"; + String in_59 = in_56 + "efg"; + String in_60 = in_56 + "efgh"; + String in_61 = in_56 + "efghi"; + + String prefix = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5emFi"; + String out_56 = prefix + "Y2Q=\n"; + String out_57 = prefix + "Y2Rl\n"; + String out_58 = prefix + "Y2Rl\nZg==\n"; + String out_59 = prefix + "Y2Rl\nZmc=\n"; + String out_60 = prefix + "Y2Rl\nZmdo\n"; + String out_61 = prefix + "Y2Rl\nZmdoaQ==\n"; + + // no newline for an empty input array. + assertEquals("", encodeToString("", 0)); + + assertEquals(out_56, encodeToString(in_56, 0)); + assertEquals(out_57, encodeToString(in_57, 0)); + assertEquals(out_58, encodeToString(in_58, 0)); + assertEquals(out_59, encodeToString(in_59, 0)); + assertEquals(out_60, encodeToString(in_60, 0)); + assertEquals(out_61, encodeToString(in_61, 0)); + + assertEquals(out_56.replaceAll("=", ""), encodeToString(in_56, Base64.NO_PADDING)); + assertEquals(out_57.replaceAll("=", ""), encodeToString(in_57, Base64.NO_PADDING)); + assertEquals(out_58.replaceAll("=", ""), encodeToString(in_58, Base64.NO_PADDING)); + assertEquals(out_59.replaceAll("=", ""), encodeToString(in_59, Base64.NO_PADDING)); + assertEquals(out_60.replaceAll("=", ""), encodeToString(in_60, Base64.NO_PADDING)); + assertEquals(out_61.replaceAll("=", ""), encodeToString(in_61, Base64.NO_PADDING)); + + assertEquals(out_56.replaceAll("\n", ""), encodeToString(in_56, Base64.NO_WRAP)); + assertEquals(out_57.replaceAll("\n", ""), encodeToString(in_57, Base64.NO_WRAP)); + assertEquals(out_58.replaceAll("\n", ""), encodeToString(in_58, Base64.NO_WRAP)); + assertEquals(out_59.replaceAll("\n", ""), encodeToString(in_59, Base64.NO_WRAP)); + assertEquals(out_60.replaceAll("\n", ""), encodeToString(in_60, Base64.NO_WRAP)); + assertEquals(out_61.replaceAll("\n", ""), encodeToString(in_61, Base64.NO_WRAP)); + } + + /** + * Tests that Base64.Encoder.encode() does correct handling of the + * tail for each call. + * + * This test is disabled because while it passes if you can get it + * to run, android's test infrastructure currently doesn't allow + * us to get at package-private members (Base64.Encoder in + * this case). + */ + public void XXXtestEncodeInternal() throws Exception { + byte[] input = { (byte) 0x61, (byte) 0x62, (byte) 0x63 }; + byte[] output = new byte[100]; + + Base64.Encoder encoder = new Base64.Encoder(Base64.NO_PADDING | Base64.NO_WRAP, + output); + + encoder.process(input, 0, 3, false); + assertEquals("YWJj".getBytes(), 4, encoder.output, encoder.op); + assertEquals(0, encoder.tailLen); + + encoder.process(input, 0, 3, false); + assertEquals("YWJj".getBytes(), 4, encoder.output, encoder.op); + assertEquals(0, encoder.tailLen); + + encoder.process(input, 0, 1, false); + assertEquals(0, encoder.op); + assertEquals(1, encoder.tailLen); + + encoder.process(input, 0, 1, false); + assertEquals(0, encoder.op); + assertEquals(2, encoder.tailLen); + + encoder.process(input, 0, 1, false); + assertEquals("YWFh".getBytes(), 4, encoder.output, encoder.op); + assertEquals(0, encoder.tailLen); + + encoder.process(input, 0, 2, false); + assertEquals(0, encoder.op); + assertEquals(2, encoder.tailLen); + + encoder.process(input, 0, 2, false); + assertEquals("YWJh".getBytes(), 4, encoder.output, encoder.op); + assertEquals(1, encoder.tailLen); + + encoder.process(input, 0, 2, false); + assertEquals("YmFi".getBytes(), 4, encoder.output, encoder.op); + assertEquals(0, encoder.tailLen); + + encoder.process(input, 0, 1, true); + assertEquals("YQ".getBytes(), 2, encoder.output, encoder.op); + } + + private static final String lipsum = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "Quisque congue eleifend odio, eu ornare nulla facilisis eget. " + + "Integer eget elit diam, sit amet laoreet nibh. Quisque enim " + + "urna, pharetra vitae consequat eget, adipiscing eu ante. " + + "Aliquam venenatis arcu nec nibh imperdiet tempor. In id dui " + + "eget lorem aliquam rutrum vel vitae eros. In placerat ornare " + + "pretium. Curabitur non fringilla mi. Fusce ultricies, turpis " + + "eu ultrices suscipit, ligula nisi consectetur eros, dapibus " + + "aliquet dui sapien a turpis. Donec ultricies varius ligula, " + + "ut hendrerit arcu malesuada at. Praesent sed elit pretium " + + "eros luctus gravida. In ac dolor lorem. Cras condimentum " + + "convallis elementum. Phasellus vel felis in nulla ultrices " + + "venenatis. Nam non tortor non orci convallis convallis. " + + "Nam tristique lacinia hendrerit. Pellentesque habitant morbi " + + "tristique senectus et netus et malesuada fames ac turpis " + + "egestas. Vivamus cursus, nibh eu imperdiet porta, magna " + + "ipsum mollis mauris, sit amet fringilla mi nisl eu mi. " + + "Phasellus posuere, leo at ultricies vehicula, massa risus " + + "volutpat sapien, eu tincidunt diam ipsum eget nulla. Cras " + + "molestie dapibus commodo. Ut vel tellus at massa gravida " + + "semper non sed orci."; + + public void testInputStream() throws Exception { + int[] flagses = { Base64.DEFAULT, + Base64.NO_PADDING, + Base64.NO_WRAP, + Base64.NO_PADDING | Base64.NO_WRAP, + Base64.CRLF, + Base64.URL_SAFE }; + int[] writeLengths = { -10, -5, -1, 0, 1, 1, 2, 2, 3, 10, 100 }; + Random rng = new Random(32176L); + + // Test input needs to be at least 2048 bytes to fill up the + // read buffer of Base64InputStream. + byte[] plain = (lipsum + lipsum + lipsum + lipsum + lipsum).getBytes(); + + for (int flags: flagses) { + byte[] encoded = Base64.encode(plain, flags); + + ByteArrayInputStream bais; + Base64InputStream b64is; + byte[] actual = new byte[plain.length * 2]; + int ap; + int b; + + // ----- test decoding ("encoded" -> "plain") ----- + + // read as much as it will give us in one chunk + bais = new ByteArrayInputStream(encoded); + b64is = new Base64InputStream(bais, flags); + ap = 0; + while ((b = b64is.read(actual, ap, actual.length-ap)) != -1) { + ap += b; + } + assertEquals(actual, ap, plain); + + // read individual bytes + bais = new ByteArrayInputStream(encoded); + b64is = new Base64InputStream(bais, flags); + ap = 0; + while ((b = b64is.read()) != -1) { + actual[ap++] = (byte) b; + } + assertEquals(actual, ap, plain); + + // mix reads of variously-sized arrays with one-byte reads + bais = new ByteArrayInputStream(encoded); + b64is = new Base64InputStream(bais, flags); + ap = 0; + readloop: while (true) { + int l = writeLengths[rng.nextInt(writeLengths.length)]; + if (l >= 0) { + b = b64is.read(actual, ap, l); + if (b == -1) break readloop; + ap += b; + } else { + for (int i = 0; i < -l; ++i) { + if ((b = b64is.read()) == -1) break readloop; + actual[ap++] = (byte) b; + } + } + } + assertEquals(actual, ap, plain); + + // ----- test encoding ("plain" -> "encoded") ----- + + // read as much as it will give us in one chunk + bais = new ByteArrayInputStream(plain); + b64is = new Base64InputStream(bais, flags, true); + ap = 0; + while ((b = b64is.read(actual, ap, actual.length-ap)) != -1) { + ap += b; + } + assertEquals(actual, ap, encoded); + + // read individual bytes + bais = new ByteArrayInputStream(plain); + b64is = new Base64InputStream(bais, flags, true); + ap = 0; + while ((b = b64is.read()) != -1) { + actual[ap++] = (byte) b; + } + assertEquals(actual, ap, encoded); + + // mix reads of variously-sized arrays with one-byte reads + bais = new ByteArrayInputStream(plain); + b64is = new Base64InputStream(bais, flags, true); + ap = 0; + readloop: while (true) { + int l = writeLengths[rng.nextInt(writeLengths.length)]; + if (l >= 0) { + b = b64is.read(actual, ap, l); + if (b == -1) break readloop; + ap += b; + } else { + for (int i = 0; i < -l; ++i) { + if ((b = b64is.read()) == -1) break readloop; + actual[ap++] = (byte) b; + } + } + } + assertEquals(actual, ap, encoded); + } + } + + /** http://b/3026478 */ + public void testSingleByteReads() throws IOException { + InputStream in = new Base64InputStream( + new ByteArrayInputStream("/v8=".getBytes()), Base64.DEFAULT); + assertEquals(254, in.read()); + assertEquals(255, in.read()); + } + + /** + * Tests that Base64OutputStream produces exactly the same results + * as calling Base64.encode/.decode on an in-memory array. + */ + public void testOutputStream() throws Exception { + int[] flagses = { Base64.DEFAULT, + Base64.NO_PADDING, + Base64.NO_WRAP, + Base64.NO_PADDING | Base64.NO_WRAP, + Base64.CRLF, + Base64.URL_SAFE }; + int[] writeLengths = { -10, -5, -1, 0, 1, 1, 2, 2, 3, 10, 100 }; + Random rng = new Random(32176L); + + // Test input needs to be at least 1024 bytes to test filling + // up the write(int) buffer of Base64OutputStream. + byte[] plain = (lipsum + lipsum).getBytes(); + + for (int flags: flagses) { + byte[] encoded = Base64.encode(plain, flags); + + ByteArrayOutputStream baos; + Base64OutputStream b64os; + byte[] actual; + int p; + + // ----- test encoding ("plain" -> "encoded") ----- + + // one large write(byte[]) of the whole input + baos = new ByteArrayOutputStream(); + b64os = new Base64OutputStream(baos, flags); + b64os.write(plain); + b64os.close(); + actual = baos.toByteArray(); + assertEquals(encoded, actual); + + // many calls to write(int) + baos = new ByteArrayOutputStream(); + b64os = new Base64OutputStream(baos, flags); + for (int i = 0; i < plain.length; ++i) { + b64os.write(plain[i]); + } + b64os.close(); + actual = baos.toByteArray(); + assertEquals(encoded, actual); + + // intermixed sequences of write(int) with + // write(byte[],int,int) of various lengths. + baos = new ByteArrayOutputStream(); + b64os = new Base64OutputStream(baos, flags); + p = 0; + while (p < plain.length) { + int l = writeLengths[rng.nextInt(writeLengths.length)]; + l = Math.min(l, plain.length-p); + if (l >= 0) { + b64os.write(plain, p, l); + p += l; + } else { + l = Math.min(-l, plain.length-p); + for (int i = 0; i < l; ++i) { + b64os.write(plain[p+i]); + } + p += l; + } + } + b64os.close(); + actual = baos.toByteArray(); + assertEquals(encoded, actual); + + // ----- test decoding ("encoded" -> "plain") ----- + + // one large write(byte[]) of the whole input + baos = new ByteArrayOutputStream(); + b64os = new Base64OutputStream(baos, flags, false); + b64os.write(encoded); + b64os.close(); + actual = baos.toByteArray(); + assertEquals(plain, actual); + + // many calls to write(int) + baos = new ByteArrayOutputStream(); + b64os = new Base64OutputStream(baos, flags, false); + for (int i = 0; i < encoded.length; ++i) { + b64os.write(encoded[i]); + } + b64os.close(); + actual = baos.toByteArray(); + assertEquals(plain, actual); + + // intermixed sequences of write(int) with + // write(byte[],int,int) of various lengths. + baos = new ByteArrayOutputStream(); + b64os = new Base64OutputStream(baos, flags, false); + p = 0; + while (p < encoded.length) { + int l = writeLengths[rng.nextInt(writeLengths.length)]; + l = Math.min(l, encoded.length-p); + if (l >= 0) { + b64os.write(encoded, p, l); + p += l; + } else { + l = Math.min(-l, encoded.length-p); + for (int i = 0; i < l; ++i) { + b64os.write(encoded[p+i]); + } + p += l; + } + } + b64os.close(); + actual = baos.toByteArray(); + assertEquals(plain, actual); + } + } +} diff --git a/src/main/java/android/util/BridgeXmlPullAttributes.java b/src/main/java/android/util/BridgeXmlPullAttributes.java new file mode 100644 index 0000000..691339e --- /dev/null +++ b/src/main/java/android/util/BridgeXmlPullAttributes.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2008 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 android.util; + +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.internal.util.XmlUtils; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.BridgeConstants; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.impl.ResourceHelper; +import com.android.resources.ResourceType; + +import org.xmlpull.v1.XmlPullParser; + +/** + * A correct implementation of the {@link AttributeSet} interface on top of a XmlPullParser + */ +public class BridgeXmlPullAttributes extends XmlPullAttributes { + + private final BridgeContext mContext; + private final boolean mPlatformFile; + + public BridgeXmlPullAttributes(XmlPullParser parser, BridgeContext context, + boolean platformFile) { + super(parser); + mContext = context; + mPlatformFile = platformFile; + } + + /* + * (non-Javadoc) + * @see android.util.XmlPullAttributes#getAttributeNameResource(int) + * + * This methods must return com.android.internal.R.attr. matching + * the name of the attribute. + * It returns 0 if it doesn't find anything. + */ + @Override + public int getAttributeNameResource(int index) { + // get the attribute name. + String name = getAttributeName(index); + + // get the attribute namespace + String ns = mParser.getAttributeNamespace(index); + + if (BridgeConstants.NS_RESOURCES.equals(ns)) { + Integer v = Bridge.getResourceId(ResourceType.ATTR, name); + if (v != null) { + return v.intValue(); + } + + return 0; + } + + // this is not an attribute in the android namespace, we query the customviewloader, if + // the namespaces match. + if (mContext.getProjectCallback().getNamespace().equals(ns)) { + Integer v = mContext.getProjectCallback().getResourceId(ResourceType.ATTR, name); + if (v != null) { + return v.intValue(); + } + } + + return 0; + } + + @Override + public int getAttributeListValue(String namespace, String attribute, + String[] options, int defaultValue) { + String value = getAttributeValue(namespace, attribute); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToList(value, options, defaultValue); + } + + return defaultValue; + } + + @Override + public boolean getAttributeBooleanValue(String namespace, String attribute, + boolean defaultValue) { + String value = getAttributeValue(namespace, attribute); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToBoolean(value, defaultValue); + } + + return defaultValue; + } + + @Override + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + String value = getAttributeValue(namespace, attribute); + + return resolveResourceValue(value, defaultValue); + } + + @Override + public int getAttributeIntValue(String namespace, String attribute, + int defaultValue) { + String value = getAttributeValue(namespace, attribute); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToInt(value, defaultValue); + } + + return defaultValue; + } + + @Override + public int getAttributeUnsignedIntValue(String namespace, String attribute, + int defaultValue) { + String value = getAttributeValue(namespace, attribute); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToUnsignedInt(value, defaultValue); + } + + return defaultValue; + } + + @Override + public float getAttributeFloatValue(String namespace, String attribute, + float defaultValue) { + String s = getAttributeValue(namespace, attribute); + if (s != null) { + ResourceValue r = getResourceValue(s); + + if (r != null) { + s = r.getValue(); + } + + return Float.parseFloat(s); + } + + return defaultValue; + } + + @Override + public int getAttributeListValue(int index, + String[] options, int defaultValue) { + return XmlUtils.convertValueToList( + getAttributeValue(index), options, defaultValue); + } + + @Override + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + String value = getAttributeValue(index); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToBoolean(value, defaultValue); + } + + return defaultValue; + } + + @Override + public int getAttributeResourceValue(int index, int defaultValue) { + String value = getAttributeValue(index); + + return resolveResourceValue(value, defaultValue); + } + + @Override + public int getAttributeIntValue(int index, int defaultValue) { + String value = getAttributeValue(index); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + if (value.charAt(0) == '#') { + return ResourceHelper.getColor(value); + } + return XmlUtils.convertValueToInt(value, defaultValue); + } + + return defaultValue; + } + + @Override + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + String value = getAttributeValue(index); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToUnsignedInt(value, defaultValue); + } + + return defaultValue; + } + + @Override + public float getAttributeFloatValue(int index, float defaultValue) { + String s = getAttributeValue(index); + if (s != null) { + ResourceValue r = getResourceValue(s); + + if (r != null) { + s = r.getValue(); + } + + return Float.parseFloat(s); + } + + return defaultValue; + } + + // -- private helper methods + + /** + * Returns a resolved {@link ResourceValue} from a given value. + */ + private ResourceValue getResourceValue(String value) { + // now look for this particular value + RenderResources resources = mContext.getRenderResources(); + return resources.resolveResValue(resources.findResValue(value, mPlatformFile)); + } + + /** + * Resolves and return a value to its associated integer. + */ + private int resolveResourceValue(String value, int defaultValue) { + ResourceValue resource = getResourceValue(value); + if (resource != null) { + Integer id = null; + if (mPlatformFile || resource.isFramework()) { + id = Bridge.getResourceId(resource.getResourceType(), resource.getName()); + } else { + id = mContext.getProjectCallback().getResourceId( + resource.getResourceType(), resource.getName()); + } + + if (id != null) { + return id; + } + } + + return defaultValue; + } +} diff --git a/src/main/java/android/util/Config.java b/src/main/java/android/util/Config.java new file mode 100644 index 0000000..70dc9aa --- /dev/null +++ b/src/main/java/android/util/Config.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2006 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 android.util; + +/** + * @deprecated This class is not useful, it just returns the same value for + * all constants, and has always done this. Do not use it. + */ +@Deprecated +public final class Config { + /** @hide */ public Config() {} + + /** + * @deprecated Always false. + */ + @Deprecated + public static final boolean DEBUG = false; + + /** + * @deprecated Always true. + */ + @Deprecated + public static final boolean RELEASE = true; + + /** + * @deprecated Always false. + */ + @Deprecated + public static final boolean PROFILE = false; + + /** + * @deprecated Always false. + */ + @Deprecated + public static final boolean LOGV = false; + + /** + * @deprecated Always true. + */ + @Deprecated + public static final boolean LOGD = true; +} diff --git a/src/main/java/android/util/ContainerHelpers.java b/src/main/java/android/util/ContainerHelpers.java new file mode 100644 index 0000000..4e5fefb --- /dev/null +++ b/src/main/java/android/util/ContainerHelpers.java @@ -0,0 +1,59 @@ +/* + * 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 android.util; + +class ContainerHelpers { + + // This is Arrays.binarySearch(), but doesn't do any argument validation. + static int binarySearch(int[] array, int size, int value) { + int lo = 0; + int hi = size - 1; + + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + final int midVal = array[mid]; + + if (midVal < value) { + lo = mid + 1; + } else if (midVal > value) { + hi = mid - 1; + } else { + return mid; // value found + } + } + return ~lo; // value not present + } + + static int binarySearch(long[] array, int size, long value) { + int lo = 0; + int hi = size - 1; + + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + final long midVal = array[mid]; + + if (midVal < value) { + lo = mid + 1; + } else if (midVal > value) { + hi = mid - 1; + } else { + return mid; // value found + } + } + return ~lo; // value not present + } +} diff --git a/src/main/java/android/util/DayOfMonthCursor.java b/src/main/java/android/util/DayOfMonthCursor.java new file mode 100644 index 0000000..393b98e --- /dev/null +++ b/src/main/java/android/util/DayOfMonthCursor.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2007 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 android.util; + +/** + * Helps control and display a month view of a calendar that has a current + * selected day. + *

      + *
    • Keeps track of current month, day, year
    • + *
    • Keeps track of current cursor position (row, column)
    • + *
    • Provides methods to help display the calendar
    • + *
    • Provides methods to move the cursor up / down / left / right.
    • + *
    + * + * This should be used by anyone who presents a month view to users and wishes + * to behave consistently with other widgets and apps; if we ever change our + * mind about when to flip the month, we can change it here only. + * + * @hide + */ +public class DayOfMonthCursor extends MonthDisplayHelper { + + private int mRow; + private int mColumn; + + /** + * @param year The initial year. + * @param month The initial month. + * @param dayOfMonth The initial dayOfMonth. + * @param weekStartDay What dayOfMonth of the week the week should start, + * in terms of {@link java.util.Calendar} constants such as + * {@link java.util.Calendar#SUNDAY}. + */ + public DayOfMonthCursor(int year, int month, int dayOfMonth, int weekStartDay) { + super(year, month, weekStartDay); + mRow = getRowOf(dayOfMonth); + mColumn = getColumnOf(dayOfMonth); + } + + + public int getSelectedRow() { + return mRow; + } + + public int getSelectedColumn() { + return mColumn; + } + + public void setSelectedRowColumn(int row, int col) { + mRow = row; + mColumn = col; + } + + public int getSelectedDayOfMonth() { + return getDayAt(mRow, mColumn); + } + + /** + * @return 0 if the selection is in the current month, otherwise -1 or +1 + * depending on whether the selection is in the first or last row. + */ + public int getSelectedMonthOffset() { + if (isWithinCurrentMonth(mRow, mColumn)) { + return 0; + } + if (mRow == 0) { + return -1; + } + return 1; + } + + public void setSelectedDayOfMonth(int dayOfMonth) { + mRow = getRowOf(dayOfMonth); + mColumn = getColumnOf(dayOfMonth); + } + + public boolean isSelected(int row, int column) { + return (mRow == row) && (mColumn == column); + } + + /** + * Move up one box, potentially flipping to the previous month. + * @return Whether the month was flipped to the previous month + * due to the move. + */ + public boolean up() { + if (isWithinCurrentMonth(mRow - 1, mColumn)) { + // within current month, just move up + mRow--; + return false; + } + // flip back to previous month, same column, first position within month + previousMonth(); + mRow = 5; + while(!isWithinCurrentMonth(mRow, mColumn)) { + mRow--; + } + return true; + } + + /** + * Move down one box, potentially flipping to the next month. + * @return Whether the month was flipped to the next month + * due to the move. + */ + public boolean down() { + if (isWithinCurrentMonth(mRow + 1, mColumn)) { + // within current month, just move down + mRow++; + return false; + } + // flip to next month, same column, first position within month + nextMonth(); + mRow = 0; + while (!isWithinCurrentMonth(mRow, mColumn)) { + mRow++; + } + return true; + } + + /** + * Move left one box, potentially flipping to the previous month. + * @return Whether the month was flipped to the previous month + * due to the move. + */ + public boolean left() { + if (mColumn == 0) { + mRow--; + mColumn = 6; + } else { + mColumn--; + } + + if (isWithinCurrentMonth(mRow, mColumn)) { + return false; + } + + // need to flip to last day of previous month + previousMonth(); + int lastDay = getNumberOfDaysInMonth(); + mRow = getRowOf(lastDay); + mColumn = getColumnOf(lastDay); + return true; + } + + /** + * Move right one box, potentially flipping to the next month. + * @return Whether the month was flipped to the next month + * due to the move. + */ + public boolean right() { + if (mColumn == 6) { + mRow++; + mColumn = 0; + } else { + mColumn++; + } + + if (isWithinCurrentMonth(mRow, mColumn)) { + return false; + } + + // need to flip to first day of next month + nextMonth(); + mRow = 0; + mColumn = 0; + while (!isWithinCurrentMonth(mRow, mColumn)) { + mColumn++; + } + return true; + } + +} diff --git a/src/main/java/android/util/DayOfMonthCursorTest.java b/src/main/java/android/util/DayOfMonthCursorTest.java new file mode 100644 index 0000000..4c5ad76 --- /dev/null +++ b/src/main/java/android/util/DayOfMonthCursorTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2007 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 android.util; + +import junit.framework.TestCase; + +import java.util.Calendar; +import android.test.suitebuilder.annotation.SmallTest; + +/** + * Unit tests for {@link DayOfMonthCursor}. + */ +public class DayOfMonthCursorTest extends TestCase { + + @SmallTest + public void testMonthRows() { + DayOfMonthCursor mc = new DayOfMonthCursor(2007, + Calendar.SEPTEMBER, 11, Calendar.SUNDAY); + + assertArraysEqual(new int[]{26, 27, 28, 29, 30, 31, 1}, + mc.getDigitsForRow(0)); + assertArraysEqual(new int[]{2, 3, 4, 5, 6, 7, 8}, + mc.getDigitsForRow(1)); + assertArraysEqual(new int[]{30, 1, 2, 3, 4, 5, 6}, + mc.getDigitsForRow(5)); + } + + @SmallTest + public void testMoveLeft() { + DayOfMonthCursor mc = new DayOfMonthCursor(2007, + Calendar.SEPTEMBER, 3, Calendar.SUNDAY); + + assertEquals(Calendar.SEPTEMBER, mc.getMonth()); + assertEquals(3, mc.getSelectedDayOfMonth()); + assertEquals(1, mc.getSelectedRow()); + assertEquals(1, mc.getSelectedColumn()); + + // move left, still same row + assertFalse(mc.left()); + assertEquals(2, mc.getSelectedDayOfMonth()); + assertEquals(1, mc.getSelectedRow()); + assertEquals(0, mc.getSelectedColumn()); + + // wrap over to previous column, same month + assertFalse(mc.left()); + assertEquals(1, mc.getSelectedDayOfMonth()); + assertEquals(0, mc.getSelectedRow()); + assertEquals(6, mc.getSelectedColumn()); + + // wrap to previous month + assertTrue(mc.left()); + assertEquals(Calendar.AUGUST, mc.getMonth()); + assertEquals(31, mc.getSelectedDayOfMonth()); + assertEquals(4, mc.getSelectedRow()); + assertEquals(5, mc.getSelectedColumn()); + } + + @SmallTest + public void testMoveRight() { + DayOfMonthCursor mc = new DayOfMonthCursor(2007, + Calendar.SEPTEMBER, 28, Calendar.SUNDAY); + + assertEquals(Calendar.SEPTEMBER, mc.getMonth()); + assertEquals(28, mc.getSelectedDayOfMonth()); + assertEquals(4, mc.getSelectedRow()); + assertEquals(5, mc.getSelectedColumn()); + + // same row + assertFalse(mc.right()); + assertEquals(29, mc.getSelectedDayOfMonth()); + assertEquals(4, mc.getSelectedRow()); + assertEquals(6, mc.getSelectedColumn()); + + // wrap to next column, same month + assertFalse(mc.right()); + assertEquals(30, mc.getSelectedDayOfMonth()); + assertEquals(5, mc.getSelectedRow()); + assertEquals(0, mc.getSelectedColumn()); + + // next month + assertTrue(mc.right()); + assertEquals(Calendar.OCTOBER, mc.getMonth()); + assertEquals(1, mc.getSelectedDayOfMonth()); + assertEquals(0, mc.getSelectedRow()); + assertEquals(1, mc.getSelectedColumn()); + } + + @SmallTest + public void testMoveUp() { + DayOfMonthCursor mc = new DayOfMonthCursor(2007, + Calendar.SEPTEMBER, 13, Calendar.SUNDAY); + + assertEquals(Calendar.SEPTEMBER, mc.getMonth()); + assertEquals(13, mc.getSelectedDayOfMonth()); + assertEquals(2, mc.getSelectedRow()); + assertEquals(4, mc.getSelectedColumn()); + + // up, same month + assertFalse(mc.up()); + assertEquals(6, mc.getSelectedDayOfMonth()); + assertEquals(1, mc.getSelectedRow()); + assertEquals(4, mc.getSelectedColumn()); + + // up, flips back + assertTrue(mc.up()); + assertEquals(Calendar.AUGUST, mc.getMonth()); + assertEquals(30, mc.getSelectedDayOfMonth()); + assertEquals(4, mc.getSelectedRow()); + assertEquals(4, mc.getSelectedColumn()); + } + + @SmallTest + public void testMoveDown() { + DayOfMonthCursor mc = new DayOfMonthCursor(2007, + Calendar.SEPTEMBER, 23, Calendar.SUNDAY); + + assertEquals(Calendar.SEPTEMBER, mc.getMonth()); + assertEquals(23, mc.getSelectedDayOfMonth()); + assertEquals(4, mc.getSelectedRow()); + assertEquals(0, mc.getSelectedColumn()); + + // down, same month + assertFalse(mc.down()); + assertEquals(30, mc.getSelectedDayOfMonth()); + assertEquals(5, mc.getSelectedRow()); + assertEquals(0, mc.getSelectedColumn()); + + // down, next month + assertTrue(mc.down()); + assertEquals(Calendar.OCTOBER, mc.getMonth()); + assertEquals(7, mc.getSelectedDayOfMonth()); + assertEquals(1, mc.getSelectedRow()); + assertEquals(0, mc.getSelectedColumn()); + } + + private void assertArraysEqual(int[] expected, int[] actual) { + assertEquals("array length", expected.length, actual.length); + for (int i = 0; i < expected.length; i++) { + assertEquals("index " + i, + expected[i], actual[i]); + } + } +} diff --git a/src/main/java/android/util/DebugUtils.java b/src/main/java/android/util/DebugUtils.java new file mode 100644 index 0000000..f607207 --- /dev/null +++ b/src/main/java/android/util/DebugUtils.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2007 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 android.util; + +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; +import java.util.Locale; + +/** + *

    Various utilities for debugging and logging.

    + */ +public class DebugUtils { + /** @hide */ public DebugUtils() {} + + /** + *

    Filters objects against the ANDROID_OBJECT_FILTER + * environment variable. This environment variable can filter objects + * based on their class name and attribute values.

    + * + *

    Here is the syntax for ANDROID_OBJECT_FILTER:

    + * + *

    ClassName@attribute1=value1@attribute2=value2...

    + * + *

    Examples:

    + *
      + *
    • Select TextView instances: TextView
    • + *
    • Select TextView instances of text "Loading" and bottom offset of 22: + * TextView@text=Loading.*@bottom=22
    • + *
    + * + *

    The class name and the values are regular expressions.

    + * + *

    This class is useful for debugging and logging purpose:

    + *
    +     * if (DEBUG) {
    +     *   if (DebugUtils.isObjectSelected(childView) && LOGV_ENABLED) {
    +     *     Log.v(TAG, "Object " + childView + " logged!");
    +     *   }
    +     * }
    +     * 
    + * + *

    NOTE: This method is very expensive as it relies + * heavily on regular expressions and reflection. Calls to this method + * should always be stripped out of the release binaries and avoided + * as much as possible in debug mode.

    + * + * @param object any object to match against the ANDROID_OBJECT_FILTER + * environement variable + * @return true if object is selected by the ANDROID_OBJECT_FILTER + * environment variable, false otherwise + */ + public static boolean isObjectSelected(Object object) { + boolean match = false; + String s = System.getenv("ANDROID_OBJECT_FILTER"); + if (s != null && s.length() > 0) { + String[] selectors = s.split("@"); + // first selector == class name + if (object.getClass().getSimpleName().matches(selectors[0])) { + // check potential attributes + for (int i = 1; i < selectors.length; i++) { + String[] pair = selectors[i].split("="); + Class klass = object.getClass(); + try { + Method declaredMethod = null; + Class parent = klass; + do { + declaredMethod = parent.getDeclaredMethod("get" + + pair[0].substring(0, 1).toUpperCase(Locale.ROOT) + + pair[0].substring(1), + (Class[]) null); + } while ((parent = klass.getSuperclass()) != null && + declaredMethod == null); + + if (declaredMethod != null) { + Object value = declaredMethod + .invoke(object, (Object[])null); + match |= (value != null ? + value.toString() : "null").matches(pair[1]); + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + } + } + return match; + } + + /** @hide */ + public static void buildShortClassTag(Object cls, StringBuilder out) { + if (cls == null) { + out.append("null"); + } else { + String simpleName = cls.getClass().getSimpleName(); + if (simpleName == null || simpleName.isEmpty()) { + simpleName = cls.getClass().getName(); + int end = simpleName.lastIndexOf('.'); + if (end > 0) { + simpleName = simpleName.substring(end+1); + } + } + out.append(simpleName); + out.append('{'); + out.append(Integer.toHexString(System.identityHashCode(cls))); + } + } + +} diff --git a/src/main/java/android/util/DisplayMetrics.java b/src/main/java/android/util/DisplayMetrics.java new file mode 100644 index 0000000..d0e5b9e --- /dev/null +++ b/src/main/java/android/util/DisplayMetrics.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2006 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 android.util; + +import android.os.SystemProperties; + + +/** + * A structure describing general information about a display, such as its + * size, density, and font scaling. + *

    To access the DisplayMetrics members, initialize an object like this:

    + *
     DisplayMetrics metrics = new DisplayMetrics();
    + * getWindowManager().getDefaultDisplay().getMetrics(metrics);
    + */ +public class DisplayMetrics { + /** + * Standard quantized DPI for low-density screens. + */ + public static final int DENSITY_LOW = 120; + + /** + * Standard quantized DPI for medium-density screens. + */ + public static final int DENSITY_MEDIUM = 160; + + /** + * This is a secondary density, added for some common screen configurations. + * It is recommended that applications not generally target this as a first + * class density -- that is, don't supply specific graphics for this + * density, instead allow the platform to scale from other densities + * (typically {@link #DENSITY_HIGH}) as + * appropriate. In most cases (such as using bitmaps in + * {@link android.graphics.drawable.Drawable}) the platform + * can perform this scaling at load time, so the only cost is some slight + * startup runtime overhead. + * + *

    This density was original introduced to correspond with a + * 720p TV screen: the density for 1080p televisions is + * {@link #DENSITY_XHIGH}, and the value here provides the same UI + * size for a TV running at 720p. It has also found use in 7" tablets, + * when these devices have 1280x720 displays. + */ + public static final int DENSITY_TV = 213; + + /** + * Standard quantized DPI for high-density screens. + */ + public static final int DENSITY_HIGH = 240; + + /** + * Intermediate density for screens that sit between {@link #DENSITY_HIGH} (240dpi) and + * {@link #DENSITY_XHIGH} (320dpi). This is not a density that applications should target, + * instead relying on the system to scale their {@link #DENSITY_XHIGH} assets for them. + */ + public static final int DENSITY_280 = 280; + + /** + * Standard quantized DPI for extra-high-density screens. + */ + public static final int DENSITY_XHIGH = 320; + + /** + * Intermediate density for screens that sit somewhere between + * {@link #DENSITY_XHIGH} (320 dpi) and {@link #DENSITY_XXHIGH} (480 dpi). + * This is not a density that applications should target, instead relying + * on the system to scale their {@link #DENSITY_XXHIGH} assets for them. + */ + public static final int DENSITY_400 = 400; + + /** + * Standard quantized DPI for extra-extra-high-density screens. + */ + public static final int DENSITY_XXHIGH = 480; + + /** + * Intermediate density for screens that sit somewhere between + * {@link #DENSITY_XXHIGH} (480 dpi) and {@link #DENSITY_XXXHIGH} (640 dpi). + * This is not a density that applications should target, instead relying + * on the system to scale their {@link #DENSITY_XXXHIGH} assets for them. + */ + public static final int DENSITY_560 = 560; + + /** + * Standard quantized DPI for extra-extra-extra-high-density screens. Applications + * should not generally worry about this density; relying on XHIGH graphics + * being scaled up to it should be sufficient for almost all cases. A typical + * use of this density would be 4K television screens -- 3840x2160, which + * is 2x a traditional HD 1920x1080 screen which runs at DENSITY_XHIGH. + */ + public static final int DENSITY_XXXHIGH = 640; + + /** + * The reference density used throughout the system. + */ + public static final int DENSITY_DEFAULT = DENSITY_MEDIUM; + + /** + * Scaling factor to convert a density in DPI units to the density scale. + * @hide + */ + public static final float DENSITY_DEFAULT_SCALE = 1.0f / DENSITY_DEFAULT; + + /** + * The device's density. + * @hide because eventually this should be able to change while + * running, so shouldn't be a constant. + * @deprecated There is no longer a static density; you can find the + * density for a display in {@link #densityDpi}. + */ + @Deprecated + public static int DENSITY_DEVICE = getDeviceDensity(); + + /** + * The absolute width of the display in pixels. + */ + public int widthPixels; + /** + * The absolute height of the display in pixels. + */ + public int heightPixels; + /** + * The logical density of the display. This is a scaling factor for the + * Density Independent Pixel unit, where one DIP is one pixel on an + * approximately 160 dpi screen (for example a 240x320, 1.5"x2" screen), + * providing the baseline of the system's display. Thus on a 160dpi screen + * this density value will be 1; on a 120 dpi screen it would be .75; etc. + * + *

    This value does not exactly follow the real screen size (as given by + * {@link #xdpi} and {@link #ydpi}, but rather is used to scale the size of + * the overall UI in steps based on gross changes in the display dpi. For + * example, a 240x320 screen will have a density of 1 even if its width is + * 1.8", 1.3", etc. However, if the screen resolution is increased to + * 320x480 but the screen size remained 1.5"x2" then the density would be + * increased (probably to 1.5). + * + * @see #DENSITY_DEFAULT + */ + public float density; + /** + * The screen density expressed as dots-per-inch. May be either + * {@link #DENSITY_LOW}, {@link #DENSITY_MEDIUM}, or {@link #DENSITY_HIGH}. + */ + public int densityDpi; + /** + * A scaling factor for fonts displayed on the display. This is the same + * as {@link #density}, except that it may be adjusted in smaller + * increments at runtime based on a user preference for the font size. + */ + public float scaledDensity; + /** + * The exact physical pixels per inch of the screen in the X dimension. + */ + public float xdpi; + /** + * The exact physical pixels per inch of the screen in the Y dimension. + */ + public float ydpi; + + /** + * The reported display width prior to any compatibility mode scaling + * being applied. + * @hide + */ + public int noncompatWidthPixels; + /** + * The reported display height prior to any compatibility mode scaling + * being applied. + * @hide + */ + public int noncompatHeightPixels; + /** + * The reported display density prior to any compatibility mode scaling + * being applied. + * @hide + */ + public float noncompatDensity; + /** + * The reported display density prior to any compatibility mode scaling + * being applied. + * @hide + */ + public int noncompatDensityDpi; + /** + * The reported scaled density prior to any compatibility mode scaling + * being applied. + * @hide + */ + public float noncompatScaledDensity; + /** + * The reported display xdpi prior to any compatibility mode scaling + * being applied. + * @hide + */ + public float noncompatXdpi; + /** + * The reported display ydpi prior to any compatibility mode scaling + * being applied. + * @hide + */ + public float noncompatYdpi; + + public DisplayMetrics() { + } + + public void setTo(DisplayMetrics o) { + widthPixels = o.widthPixels; + heightPixels = o.heightPixels; + density = o.density; + densityDpi = o.densityDpi; + scaledDensity = o.scaledDensity; + xdpi = o.xdpi; + ydpi = o.ydpi; + noncompatWidthPixels = o.noncompatWidthPixels; + noncompatHeightPixels = o.noncompatHeightPixels; + noncompatDensity = o.noncompatDensity; + noncompatDensityDpi = o.noncompatDensityDpi; + noncompatScaledDensity = o.noncompatScaledDensity; + noncompatXdpi = o.noncompatXdpi; + noncompatYdpi = o.noncompatYdpi; + } + + public void setToDefaults() { + widthPixels = 0; + heightPixels = 0; + density = DENSITY_DEVICE / (float) DENSITY_DEFAULT; + densityDpi = DENSITY_DEVICE; + scaledDensity = density; + xdpi = DENSITY_DEVICE; + ydpi = DENSITY_DEVICE; + noncompatWidthPixels = widthPixels; + noncompatHeightPixels = heightPixels; + noncompatDensity = density; + noncompatDensityDpi = densityDpi; + noncompatScaledDensity = scaledDensity; + noncompatXdpi = xdpi; + noncompatYdpi = ydpi; + } + + @Override + public boolean equals(Object o) { + return o instanceof DisplayMetrics && equals((DisplayMetrics)o); + } + + /** + * Returns true if these display metrics equal the other display metrics. + * + * @param other The display metrics with which to compare. + * @return True if the display metrics are equal. + */ + public boolean equals(DisplayMetrics other) { + return equalsPhysical(other) + && scaledDensity == other.scaledDensity + && noncompatScaledDensity == other.noncompatScaledDensity; + } + + /** + * Returns true if the physical aspects of the two display metrics + * are equal. This ignores the scaled density, which is a logical + * attribute based on the current desired font size. + * + * @param other The display metrics with which to compare. + * @return True if the display metrics are equal. + * @hide + */ + public boolean equalsPhysical(DisplayMetrics other) { + return other != null + && widthPixels == other.widthPixels + && heightPixels == other.heightPixels + && density == other.density + && densityDpi == other.densityDpi + && xdpi == other.xdpi + && ydpi == other.ydpi + && noncompatWidthPixels == other.noncompatWidthPixels + && noncompatHeightPixels == other.noncompatHeightPixels + && noncompatDensity == other.noncompatDensity + && noncompatDensityDpi == other.noncompatDensityDpi + && noncompatXdpi == other.noncompatXdpi + && noncompatYdpi == other.noncompatYdpi; + } + + @Override + public int hashCode() { + return widthPixels * heightPixels * densityDpi; + } + + @Override + public String toString() { + return "DisplayMetrics{density=" + density + ", width=" + widthPixels + + ", height=" + heightPixels + ", scaledDensity=" + scaledDensity + + ", xdpi=" + xdpi + ", ydpi=" + ydpi + "}"; + } + + private static int getDeviceDensity() { + // qemu.sf.lcd_density can be used to override ro.sf.lcd_density + // when running in the emulator, allowing for dynamic configurations. + // The reason for this is that ro.sf.lcd_density is write-once and is + // set by the init process when it parses build.prop before anything else. + return SystemProperties.getInt("qemu.sf.lcd_density", + SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT)); + } +} diff --git a/src/main/java/android/util/EventLog.java b/src/main/java/android/util/EventLog.java new file mode 100644 index 0000000..aefced8 --- /dev/null +++ b/src/main/java/android/util/EventLog.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2007 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 android.util; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Collection; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Access to the system diagnostic event record. System diagnostic events are + * used to record certain system-level events (such as garbage collection, + * activity manager state, system watchdogs, and other low level activity), + * which may be automatically collected and analyzed during system development. + * + *

    This is not the main "logcat" debugging log ({@link android.util.Log})! + * These diagnostic events are for system integrators, not application authors. + * + *

    Events use integer tag codes corresponding to /system/etc/event-log-tags. + * They carry a payload of one or more int, long, or String values. The + * event-log-tags file defines the payload contents for each type code. + */ +public class EventLog { + /** @hide */ public EventLog() {} + + private static final String TAG = "EventLog"; + + private static final String TAGS_FILE = "/system/etc/event-log-tags"; + private static final String COMMENT_PATTERN = "^\\s*(#.*)?$"; + private static final String TAG_PATTERN = "^\\s*(\\d+)\\s+(\\w+)\\s*(\\(.*\\))?\\s*$"; + private static HashMap sTagCodes = null; + private static HashMap sTagNames = null; + + /** A previously logged event read from the logs. */ + public static final class Event { + private final ByteBuffer mBuffer; + + // Layout of event log entry received from Android logger. + // see system/core/include/log/logger.h + private static final int LENGTH_OFFSET = 0; + private static final int HEADER_SIZE_OFFSET = 2; + private static final int PROCESS_OFFSET = 4; + private static final int THREAD_OFFSET = 8; + private static final int SECONDS_OFFSET = 12; + private static final int NANOSECONDS_OFFSET = 16; + + // Layout for event log v1 format, v2 and v3 use HEADER_SIZE_OFFSET + private static final int V1_PAYLOAD_START = 20; + private static final int DATA_OFFSET = 4; + + // Value types + private static final byte INT_TYPE = 0; + private static final byte LONG_TYPE = 1; + private static final byte STRING_TYPE = 2; + private static final byte LIST_TYPE = 3; + + /** @param data containing event, read from the system */ + /*package*/ Event(byte[] data) { + mBuffer = ByteBuffer.wrap(data); + mBuffer.order(ByteOrder.nativeOrder()); + } + + /** @return the process ID which wrote the log entry */ + public int getProcessId() { + return mBuffer.getInt(PROCESS_OFFSET); + } + + /** @return the thread ID which wrote the log entry */ + public int getThreadId() { + return mBuffer.getInt(THREAD_OFFSET); + } + + /** @return the wall clock time when the entry was written */ + public long getTimeNanos() { + return mBuffer.getInt(SECONDS_OFFSET) * 1000000000l + + mBuffer.getInt(NANOSECONDS_OFFSET); + } + + /** @return the type tag code of the entry */ + public int getTag() { + int offset = mBuffer.getShort(HEADER_SIZE_OFFSET); + if (offset == 0) { + offset = V1_PAYLOAD_START; + } + return mBuffer.getInt(offset); + } + + /** @return one of Integer, Long, String, null, or Object[] of same. */ + public synchronized Object getData() { + try { + int offset = mBuffer.getShort(HEADER_SIZE_OFFSET); + if (offset == 0) { + offset = V1_PAYLOAD_START; + } + mBuffer.limit(offset + mBuffer.getShort(LENGTH_OFFSET)); + mBuffer.position(offset + DATA_OFFSET); // Just after the tag. + return decodeObject(); + } catch (IllegalArgumentException e) { + Log.wtf(TAG, "Illegal entry payload: tag=" + getTag(), e); + return null; + } catch (BufferUnderflowException e) { + Log.wtf(TAG, "Truncated entry payload: tag=" + getTag(), e); + return null; + } + } + + /** @return the loggable item at the current position in mBuffer. */ + private Object decodeObject() { + byte type = mBuffer.get(); + switch (type) { + case INT_TYPE: + return (Integer) mBuffer.getInt(); + + case LONG_TYPE: + return (Long) mBuffer.getLong(); + + case STRING_TYPE: + try { + int length = mBuffer.getInt(); + int start = mBuffer.position(); + mBuffer.position(start + length); + return new String(mBuffer.array(), start, length, "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.wtf(TAG, "UTF-8 is not supported", e); + return null; + } + + case LIST_TYPE: + int length = mBuffer.get(); + if (length < 0) length += 256; // treat as signed byte + Object[] array = new Object[length]; + for (int i = 0; i < length; ++i) array[i] = decodeObject(); + return array; + + default: + throw new IllegalArgumentException("Unknown entry type: " + type); + } + } + } + + // We assume that the native methods deal with any concurrency issues. + + /** + * Record an event log message. + * @param tag The event type tag code + * @param value A value to log + * @return The number of bytes written + */ + public static native int writeEvent(int tag, int value); + + /** + * Record an event log message. + * @param tag The event type tag code + * @param value A value to log + * @return The number of bytes written + */ + public static native int writeEvent(int tag, long value); + + /** + * Record an event log message. + * @param tag The event type tag code + * @param str A value to log + * @return The number of bytes written + */ + public static native int writeEvent(int tag, String str); + + /** + * Record an event log message. + * @param tag The event type tag code + * @param list A list of values to log + * @return The number of bytes written + */ + public static native int writeEvent(int tag, Object... list); + + /** + * Read events from the log, filtered by type. + * @param tags to search for + * @param output container to add events into + * @throws IOException if something goes wrong reading events + */ + public static native void readEvents(int[] tags, Collection output) + throws IOException; + + /** + * Get the name associated with an event type tag code. + * @param tag code to look up + * @return the name of the tag, or null if no tag has that number + */ + public static String getTagName(int tag) { + readTagsFile(); + return sTagNames.get(tag); + } + + /** + * Get the event type tag code associated with an event name. + * @param name of event to look up + * @return the tag code, or -1 if no tag has that name + */ + public static int getTagCode(String name) { + readTagsFile(); + Integer code = sTagCodes.get(name); + return code != null ? code : -1; + } + + /** + * Read TAGS_FILE, populating sTagCodes and sTagNames, if not already done. + */ + private static synchronized void readTagsFile() { + if (sTagCodes != null && sTagNames != null) return; + + sTagCodes = new HashMap(); + sTagNames = new HashMap(); + + Pattern comment = Pattern.compile(COMMENT_PATTERN); + Pattern tag = Pattern.compile(TAG_PATTERN); + BufferedReader reader = null; + String line; + + try { + reader = new BufferedReader(new FileReader(TAGS_FILE), 256); + while ((line = reader.readLine()) != null) { + if (comment.matcher(line).matches()) continue; + + Matcher m = tag.matcher(line); + if (!m.matches()) { + Log.wtf(TAG, "Bad entry in " + TAGS_FILE + ": " + line); + continue; + } + + try { + int num = Integer.parseInt(m.group(1)); + String name = m.group(2); + sTagCodes.put(name, num); + sTagNames.put(num, name); + } catch (NumberFormatException e) { + Log.wtf(TAG, "Error in " + TAGS_FILE + ": " + line, e); + } + } + } catch (IOException e) { + Log.wtf(TAG, "Error reading " + TAGS_FILE, e); + // Leave the maps existing but unpopulated + } finally { + try { if (reader != null) reader.close(); } catch (IOException e) {} + } + } +} diff --git a/src/main/java/android/util/EventLogTags.java b/src/main/java/android/util/EventLogTags.java new file mode 100644 index 0000000..f4ce4fd --- /dev/null +++ b/src/main/java/android/util/EventLogTags.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2008 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 android.util; + +import java.io.BufferedReader; +import java.io.IOException; + +/** + * @deprecated This class is no longer functional. + * Use {@link android.util.EventLog} instead. + */ +@Deprecated +public class EventLogTags { + public static class Description { + public final int mTag; + public final String mName; + + Description(int tag, String name) { + mTag = tag; + mName = name; + } + } + + public EventLogTags() throws IOException {} + + public EventLogTags(BufferedReader input) throws IOException {} + + public Description get(String name) { return null; } + + public Description get(int tag) { return null; } +} diff --git a/src/main/java/android/util/ExceptionUtils.java b/src/main/java/android/util/ExceptionUtils.java new file mode 100644 index 0000000..f5d515d --- /dev/null +++ b/src/main/java/android/util/ExceptionUtils.java @@ -0,0 +1,58 @@ +/* + * 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 android.util; + +import java.io.IOException; + +/** + * Utility methods for proxying richer exceptions across Binder calls. + * + * @hide + */ +public class ExceptionUtils { + // TODO: longer term these should be replaced with first-class + // Parcel.read/writeException() and AIDL support, but for now do this using + // a nasty hack. + + private static final String PREFIX_IO = "\u2603"; + + public static RuntimeException wrap(IOException e) { + throw new IllegalStateException(PREFIX_IO + e.getMessage()); + } + + public static void maybeUnwrapIOException(RuntimeException e) throws IOException { + if ((e instanceof IllegalStateException) && e.getMessage().startsWith(PREFIX_IO)) { + throw new IOException(e.getMessage().substring(PREFIX_IO.length())); + } + } + + public static String getCompleteMessage(String msg, Throwable t) { + final StringBuilder builder = new StringBuilder(); + if (msg != null) { + builder.append(msg).append(": "); + } + builder.append(t.getMessage()); + while ((t = t.getCause()) != null) { + builder.append(": ").append(t.getMessage()); + } + return builder.toString(); + } + + public static String getCompleteMessage(Throwable t) { + return getCompleteMessage(null, t); + } +} diff --git a/src/main/java/android/util/FastImmutableArraySet.java b/src/main/java/android/util/FastImmutableArraySet.java new file mode 100644 index 0000000..4175c60 --- /dev/null +++ b/src/main/java/android/util/FastImmutableArraySet.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011 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 android.util; + +import java.util.AbstractSet; +import java.util.Iterator; + +/** + * A fast immutable set wrapper for an array that is optimized for non-concurrent iteration. + * The same iterator instance is reused each time to avoid creating lots of garbage. + * Iterating over an array in this fashion is 2.5x faster than iterating over a {@link HashSet} + * so it is worth copying the contents of the set to an array when iterating over it + * hundreds of times. + * @hide + */ +public final class FastImmutableArraySet extends AbstractSet { + FastIterator mIterator; + T[] mContents; + + public FastImmutableArraySet(T[] contents) { + mContents = contents; + } + + @Override + public Iterator iterator() { + FastIterator it = mIterator; + if (it == null) { + it = new FastIterator(mContents); + mIterator = it; + } else { + it.mIndex = 0; + } + return it; + } + + @Override + public int size() { + return mContents.length; + } + + private static final class FastIterator implements Iterator { + private final T[] mContents; + int mIndex; + + public FastIterator(T[] contents) { + mContents = contents; + } + + @Override + public boolean hasNext() { + return mIndex != mContents.length; + } + + @Override + public T next() { + return mContents[mIndex++]; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/main/java/android/util/FloatMath.java b/src/main/java/android/util/FloatMath.java new file mode 100644 index 0000000..bdcf5ca --- /dev/null +++ b/src/main/java/android/util/FloatMath.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2007 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 android.util; + +/** + * Math routines similar to those found in {@link java.lang.Math}. On + * versions of Android with a JIT, these are significantly slower than + * the equivalent {@code Math} functions, which should be used in preference + * to these. + * + * @deprecated Use {@link java.lang.Math} instead. + */ +@Deprecated +public class FloatMath { + + /** Prevents instantiation. */ + private FloatMath() {} + + /** + * Returns the float conversion of the most positive (i.e. closest to + * positive infinity) integer value which is less than the argument. + * + * @param value to be converted + * @return the floor of value + */ + public static native float floor(float value); + + /** + * Returns the float conversion of the most negative (i.e. closest to + * negative infinity) integer value which is greater than the argument. + * + * @param value to be converted + * @return the ceiling of value + */ + public static native float ceil(float value); + + /** + * Returns the closest float approximation of the sine of the argument. + * + * @param angle to compute the cosine of, in radians + * @return the sine of angle + */ + public static native float sin(float angle); + + /** + * Returns the closest float approximation of the cosine of the argument. + * + * @param angle to compute the cosine of, in radians + * @return the cosine of angle + */ + public static native float cos(float angle); + + /** + * Returns the closest float approximation of the square root of the + * argument. + * + * @param value to compute sqrt of + * @return the square root of value + */ + public static native float sqrt(float value); + + /** + * Returns the closest float approximation of the raising "e" to the power + * of the argument. + * + * @param value to compute the exponential of + * @return the exponential of value + */ + public static native float exp(float value); + + /** + * Returns the closest float approximation of the result of raising {@code + * x} to the power of {@code y}. + * + * @param x the base of the operation. + * @param y the exponent of the operation. + * @return {@code x} to the power of {@code y}. + */ + public static native float pow(float x, float y); + + /** + * Returns {@code sqrt(}{@code x}{@code 2}{@code +} + * {@code y}{@code 2}{@code )}. + * + * @param x a float number + * @param y a float number + * @return the hypotenuse + */ + public static native float hypot(float x, float y); +} diff --git a/src/main/java/android/util/FloatMathTest.java b/src/main/java/android/util/FloatMathTest.java new file mode 100644 index 0000000..f479e2b --- /dev/null +++ b/src/main/java/android/util/FloatMathTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2007 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 android.util; + +import junit.framework.TestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class FloatMathTest extends TestCase { + + @SmallTest + public void testSqrt() { + assertEquals(7, FloatMath.sqrt(49), 0); + assertEquals(10, FloatMath.sqrt(100), 0); + assertEquals(0, FloatMath.sqrt(0), 0); + assertEquals(1, FloatMath.sqrt(1), 0); + } + + @SmallTest + public void testFloor() { + assertEquals(78, FloatMath.floor(78.89f), 0); + assertEquals(-79, FloatMath.floor(-78.89f), 0); + } + + @SmallTest + public void testCeil() { + assertEquals(79, FloatMath.ceil(78.89f), 0); + assertEquals(-78, FloatMath.ceil(-78.89f), 0); + } + + @SmallTest + public void testSin() { + assertEquals(0.0, FloatMath.sin(0), 0); + assertEquals(0.8414709848078965f, FloatMath.sin(1), 0); + } + + @SmallTest + public void testCos() { + assertEquals(1.0f, FloatMath.cos(0), 0); + assertEquals(0.5403023058681398f, FloatMath.cos(1), 0); + } +} diff --git a/src/main/java/android/util/FloatMath_Delegate.java b/src/main/java/android/util/FloatMath_Delegate.java new file mode 100644 index 0000000..8b4c60b --- /dev/null +++ b/src/main/java/android/util/FloatMath_Delegate.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2007 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 android.util; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.util.FloatMath + * + * Through the layoutlib_create tool, the original native methods of FloatMath have been replaced + * by calls to methods of the same name in this delegate class. + * + * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager} + * around to map int to instance of the delegate. + * + */ +/*package*/ final class FloatMath_Delegate { + + /** Prevents instantiation. */ + private FloatMath_Delegate() {} + + /** + * Returns the float conversion of the most positive (i.e. closest to + * positive infinity) integer value which is less than the argument. + * + * @param value to be converted + * @return the floor of value + */ + @LayoutlibDelegate + /*package*/ static float floor(float value) { + return (float)Math.floor(value); + } + + /** + * Returns the float conversion of the most negative (i.e. closest to + * negative infinity) integer value which is greater than the argument. + * + * @param value to be converted + * @return the ceiling of value + */ + @LayoutlibDelegate + /*package*/ static float ceil(float value) { + return (float)Math.ceil(value); + } + + /** + * Returns the closest float approximation of the sine of the argument. + * + * @param angle to compute the cosine of, in radians + * @return the sine of angle + */ + @LayoutlibDelegate + /*package*/ static float sin(float angle) { + return (float)Math.sin(angle); + } + + /** + * Returns the closest float approximation of the cosine of the argument. + * + * @param angle to compute the cosine of, in radians + * @return the cosine of angle + */ + @LayoutlibDelegate + /*package*/ static float cos(float angle) { + return (float)Math.cos(angle); + } + + /** + * Returns the closest float approximation of the square root of the + * argument. + * + * @param value to compute sqrt of + * @return the square root of value + */ + @LayoutlibDelegate + /*package*/ static float sqrt(float value) { + return (float)Math.sqrt(value); + } + + /** + * Returns the closest float approximation of the raising "e" to the power + * of the argument. + * + * @param value to compute the exponential of + * @return the exponential of value + */ + @LayoutlibDelegate + /*package*/ static float exp(float value) { + return (float)Math.exp(value); + } + + /** + * Returns the closest float approximation of the result of raising {@code + * x} to the power of {@code y}. + * + * @param x the base of the operation. + * @param y the exponent of the operation. + * @return {@code x} to the power of {@code y}. + */ + @LayoutlibDelegate + /*package*/ static float pow(float x, float y) { + return (float)Math.pow(x, y); + } + + /** + * Returns {@code sqrt(}{@code x}{@code 2}{@code +} + * {@code y}{@code 2}{@code )}. + * + * @param x a float number + * @param y a float number + * @return the hypotenuse + */ + @LayoutlibDelegate + /*package*/ static float hypot(float x, float y) { + return (float)Math.sqrt(x*x + y*y); + } +} diff --git a/src/main/java/android/util/FloatProperty.java b/src/main/java/android/util/FloatProperty.java new file mode 100644 index 0000000..a67b3cb --- /dev/null +++ b/src/main/java/android/util/FloatProperty.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 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 android.util; + +import android.util.Property; + +/** + * An implementation of {@link android.util.Property} to be used specifically with fields of type + * float. This type-specific subclass enables performance benefit by allowing + * calls to a {@link #set(Object, Float) set()} function that takes the primitive + * float type and avoids autoboxing and other overhead associated with the + * Float class. + * + * @param The class on which the Property is declared. + * + * @hide + */ +public abstract class FloatProperty extends Property { + + public FloatProperty(String name) { + super(Float.class, name); + } + + /** + * A type-specific override of the {@link #set(Object, Float)} that is faster when dealing + * with fields of type float. + */ + public abstract void setValue(T object, float value); + + @Override + final public void set(T object, Float value) { + setValue(object, value); + } + +} \ No newline at end of file diff --git a/src/main/java/android/util/GridScenario.java b/src/main/java/android/util/GridScenario.java new file mode 100644 index 0000000..0f1730e --- /dev/null +++ b/src/main/java/android/util/GridScenario.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2007 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 android.util; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import com.google.android.collect.Maps; + +import java.util.Map; + +/** + * Utility base class for creating various GridView scenarios. Configurable by the number + * of items, how tall each item should be (in relation to the screen height), and + * what item should start with selection. + */ +public abstract class GridScenario extends Activity { + + private GridView mGridView; + + private int mNumItems; + + private int mStartingSelectionPosition; + private double mItemScreenSizeFactor; + private Map mOverrideItemScreenSizeFactors = Maps.newHashMap(); + + private int mScreenHeight; + + private boolean mStackFromBottom; + + private int mColumnWidth; + + private int mNumColumns; + + private int mStretchMode; + + private int mVerticalSpacing; + + public GridView getGridView() { + return mGridView; + } + + protected int getScreenHeight() { + return mScreenHeight; + } + + /** + * @return The initial number of items in the grid as specified by the scenario. + * This number may change over time. + */ + protected int getInitialNumItems() { + return mNumItems; + } + + /** + * @return The desired height of 1 item, ignoring overrides + */ + public int getDesiredItemHeight() { + return (int) (mScreenHeight * mItemScreenSizeFactor); + } + + /** + * Better way to pass in optional params than a honkin' paramater list :) + */ + public static class Params { + private int mNumItems = 4; + private int mStartingSelectionPosition = -1; + private double mItemScreenSizeFactor = 1 / 5; + + private Map mOverrideItemScreenSizeFactors = Maps.newHashMap(); + + private boolean mStackFromBottom = false; + private boolean mMustFillScreen = true; + + private int mColumnWidth = 0; + private int mNumColumns = GridView.AUTO_FIT; + private int mStretchMode = GridView.STRETCH_COLUMN_WIDTH; + private int mVerticalSpacing = 0; + + /** + * Set the number of items in the grid. + */ + public Params setNumItems(int numItems) { + mNumItems = numItems; + return this; + } + + /** + * Set the position that starts selected. + * + * @param startingSelectionPosition The selected position within the adapter's data set. + * Pass -1 if you do not want to force a selection. + * @return + */ + public Params setStartingSelectionPosition(int startingSelectionPosition) { + mStartingSelectionPosition = startingSelectionPosition; + return this; + } + + /** + * Set the factor that determines how tall each item is in relation to the + * screen height. + */ + public Params setItemScreenSizeFactor(double itemScreenSizeFactor) { + mItemScreenSizeFactor = itemScreenSizeFactor; + return this; + } + + /** + * Override the item screen size factor for a particular item. Useful for + * creating grids with non-uniform item height. + * @param position The position in the grid. + * @param itemScreenSizeFactor The screen size factor to use for the height. + */ + public Params setPositionScreenSizeFactorOverride( + int position, double itemScreenSizeFactor) { + mOverrideItemScreenSizeFactors.put(position, itemScreenSizeFactor); + return this; + } + + /** + * Sets the stacking direction + * @param stackFromBottom + * @return + */ + public Params setStackFromBottom(boolean stackFromBottom) { + mStackFromBottom = stackFromBottom; + return this; + } + + /** + * Sets whether the sum of the height of the grid items must be at least the + * height of the grid view. + */ + public Params setMustFillScreen(boolean fillScreen) { + mMustFillScreen = fillScreen; + return this; + } + + /** + * Sets the individual width of each column. + * + * @param requestedWidth the width in pixels of the column + */ + public Params setColumnWidth(int requestedWidth) { + mColumnWidth = requestedWidth; + return this; + } + + /** + * Sets the number of columns in the grid. + */ + public Params setNumColumns(int numColumns) { + mNumColumns = numColumns; + return this; + } + + /** + * Sets the stretch mode. + */ + public Params setStretchMode(int stretchMode) { + mStretchMode = stretchMode; + return this; + } + + /** + * Sets the spacing between rows in the grid + */ + public Params setVerticalSpacing(int verticalSpacing) { + mVerticalSpacing = verticalSpacing; + return this; + } + } + + /** + * How each scenario customizes its behavior. + * @param params + */ + protected abstract void init(Params params); + + /** + * Override this to provide an different adapter for your scenario + * @return The adapter that this scenario will use + */ + protected ListAdapter createAdapter() { + return new MyAdapter(); + } + + /** + * Override this if you want to know when something has been selected (perhaps + * more importantly, that {@link android.widget.AdapterView.OnItemSelectedListener} has + * been triggered). + */ + @SuppressWarnings({ "UnusedDeclaration" }) + protected void positionSelected(int positon) { + + } + + /** + * Override this if you want to know that nothing is selected. + */ + protected void nothingSelected() { + + } + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // turn off title bar + requestWindowFeature(Window.FEATURE_NO_TITLE); + + mScreenHeight = getWindowManager().getDefaultDisplay().getHeight(); + + final Params params = new Params(); + init(params); + + readAndValidateParams(params); + + mGridView = new GridView(this); + mGridView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mGridView.setDrawSelectorOnTop(false); + if (mNumColumns >= GridView.AUTO_FIT) { + mGridView.setNumColumns(mNumColumns); + } + if (mColumnWidth > 0) { + mGridView.setColumnWidth(mColumnWidth); + } + if (mVerticalSpacing > 0) { + mGridView.setVerticalSpacing(mVerticalSpacing); + } + mGridView.setStretchMode(mStretchMode); + mGridView.setAdapter(createAdapter()); + if (mStartingSelectionPosition >= 0) { + mGridView.setSelection(mStartingSelectionPosition); + } + mGridView.setPadding(10, 10, 10, 10); + mGridView.setStackFromBottom(mStackFromBottom); + + mGridView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View v, int position, long id) { + positionSelected(position); + } + + public void onNothingSelected(AdapterView parent) { + nothingSelected(); + } + }); + + setContentView(mGridView); + } + + + + /** + * Read in and validate all of the params passed in by the scenario. + * @param params + */ + private void readAndValidateParams(Params params) { + if (params.mMustFillScreen ) { + double totalFactor = 0.0; + for (int i = 0; i < params.mNumItems; i++) { + if (params.mOverrideItemScreenSizeFactors.containsKey(i)) { + totalFactor += params.mOverrideItemScreenSizeFactors.get(i); + } else { + totalFactor += params.mItemScreenSizeFactor; + } + } + if (totalFactor < 1.0) { + throw new IllegalArgumentException("grid items must combine to be at least " + + "the height of the screen. this is not the case with " + params.mNumItems + + " items and " + params.mItemScreenSizeFactor + " screen factor and " + + "screen height of " + mScreenHeight); + } + } + + mNumItems = params.mNumItems; + mStartingSelectionPosition = params.mStartingSelectionPosition; + mItemScreenSizeFactor = params.mItemScreenSizeFactor; + + mOverrideItemScreenSizeFactors.putAll(params.mOverrideItemScreenSizeFactors); + + mStackFromBottom = params.mStackFromBottom; + mColumnWidth = params.mColumnWidth; + mNumColumns = params.mNumColumns; + mStretchMode = params.mStretchMode; + mVerticalSpacing = params.mVerticalSpacing; + } + + public final String getValueAtPosition(int position) { + return "postion " + position; + } + + /** + * Create a view for a grid item. Override this to create a custom view beyond + * the simple focusable / unfocusable text view. + * @param position The position. + * @param parent The parent + * @param desiredHeight The height the view should be to respect the desired item + * to screen height ratio. + * @return a view for the grid. + */ + protected View createView(int position, ViewGroup parent, int desiredHeight) { + TextView result = new TextView(parent.getContext()); + result.setHeight(desiredHeight); + result.setText(getValueAtPosition(position)); + final ViewGroup.LayoutParams lp = new AbsListView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + result.setLayoutParams(lp); + result.setId(position); + result.setBackgroundColor(0x55ffffff); + return result; + } + + + + private class MyAdapter extends BaseAdapter { + public int getCount() { + return mNumItems; + } + + public Object getItem(int position) { + return getValueAtPosition(position); + } + + public long getItemId(int position) { + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView != null) { + ((TextView) convertView).setText(getValueAtPosition(position)); + convertView.setId(position); + return convertView; + } + + int desiredHeight = getDesiredItemHeight(); + if (mOverrideItemScreenSizeFactors.containsKey(position)) { + desiredHeight = (int) (mScreenHeight * mOverrideItemScreenSizeFactors.get(position)); + } + return createView(position, parent, desiredHeight); + } + } +} diff --git a/src/main/java/android/util/IntArray.java b/src/main/java/android/util/IntArray.java new file mode 100644 index 0000000..e8d3947 --- /dev/null +++ b/src/main/java/android/util/IntArray.java @@ -0,0 +1,162 @@ +/* + * 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 android.util; + +import com.android.internal.util.ArrayUtils; + +import libcore.util.EmptyArray; + +/** + * Implements a growing array of int primitives. + * + * @hide + */ +public class IntArray implements Cloneable { + private static final int MIN_CAPACITY_INCREMENT = 12; + + private int[] mValues; + private int mSize; + + /** + * Creates an empty IntArray with the default initial capacity. + */ + public IntArray() { + this(10); + } + + /** + * Creates an empty IntArray with the specified initial capacity. + */ + public IntArray(int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.INT; + } else { + mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity); + } + mSize = 0; + } + + /** + * Appends the specified value to the end of this array. + */ + public void add(int value) { + add(mSize, value); + } + + /** + * Inserts a value at the specified position in this array. + * + * @throws IndexOutOfBoundsException when index < 0 || index > size() + */ + public void add(int index, int value) { + if (index < 0 || index > mSize) { + throw new IndexOutOfBoundsException(); + } + + ensureCapacity(1); + + if (mSize - index != 0) { + System.arraycopy(mValues, index, mValues, index + 1, mSize - index); + } + + mValues[index] = value; + mSize++; + } + + /** + * Adds the values in the specified array to this array. + */ + public void addAll(IntArray values) { + final int count = values.mSize; + ensureCapacity(count); + + System.arraycopy(values.mValues, 0, mValues, mSize, count); + mSize += count; + } + + /** + * Ensures capacity to append at least count values. + */ + private void ensureCapacity(int count) { + final int currentSize = mSize; + final int minCapacity = currentSize + count; + if (minCapacity >= mValues.length) { + final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ? + MIN_CAPACITY_INCREMENT : currentSize >> 1); + final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity; + final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, currentSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + @Override + public IntArray clone() throws CloneNotSupportedException { + final IntArray clone = (IntArray) super.clone(); + clone.mValues = mValues.clone(); + return clone; + } + + /** + * Returns the value at the specified position in this array. + */ + public int get(int index) { + if (index >= mSize) { + throw new ArrayIndexOutOfBoundsException(mSize, index); + } + return mValues[index]; + } + + /** + * Returns the index of the first occurrence of the specified value in this + * array, or -1 if this array does not contain the value. + */ + public int indexOf(int value) { + final int n = mSize; + for (int i = 0; i < n; i++) { + if (mValues[i] == value) { + return i; + } + } + return -1; + } + + /** + * Removes the value at the specified index from this array. + */ + public void remove(int index) { + if (index >= mSize) { + throw new ArrayIndexOutOfBoundsException(mSize, index); + } + System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1); + mSize--; + } + + /** + * Returns the number of values in this array. + */ + public int size() { + return mSize; + } +} diff --git a/src/main/java/android/util/IntProperty.java b/src/main/java/android/util/IntProperty.java new file mode 100644 index 0000000..17977ca --- /dev/null +++ b/src/main/java/android/util/IntProperty.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 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 android.util; + +import android.util.Property; + +/** + * An implementation of {@link android.util.Property} to be used specifically with fields of type + * int. This type-specific subclass enables performance benefit by allowing + * calls to a {@link #set(Object, Integer) set()} function that takes the primitive + * int type and avoids autoboxing and other overhead associated with the + * Integer class. + * + * @param The class on which the Property is declared. + * + * @hide + */ +public abstract class IntProperty extends Property { + + public IntProperty(String name) { + super(Integer.class, name); + } + + /** + * A type-specific override of the {@link #set(Object, Integer)} that is faster when dealing + * with fields of type int. + */ + public abstract void setValue(T object, int value); + + @Override + final public void set(T object, Integer value) { + setValue(object, value.intValue()); + } + +} \ No newline at end of file diff --git a/src/main/java/android/util/InternalSelectionView.java b/src/main/java/android/util/InternalSelectionView.java new file mode 100644 index 0000000..a0fb0f1 --- /dev/null +++ b/src/main/java/android/util/InternalSelectionView.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2007 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 android.util; + +import com.android.frameworks.coretests.R; + +import android.view.View; +import android.view.KeyEvent; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Paint; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Color; +import android.util.AttributeSet; + + + +/** + * A view that has a known number of selectable rows, and maintains a notion of which + * row is selected. The rows take up the + * entire width of the view. The height of the view is divided evenly among + * the rows. + * + * Note: If the height of the view does not divide exactly to the number of rows, + * the last row's height is inflated with the remainder. For example, if the + * view height is 22 and there are two rows, the height of the first row is + * 10 and the second 22. + * + * Notice what this view does to be a good citizen w.r.t its internal selection: + * 1) calls {@link View#requestRectangleOnScreen} each time the selection changes due to + * internal navigation. + * 2) implements {@link View#getFocusedRect} by filling in the rectangle of the currently + * selected row + * 3) overrides {@link View#onFocusChanged} and sets selection appropriately according to + * the previously focused rectangle. + */ +public class InternalSelectionView extends View { + + private Paint mPainter = new Paint(); + private Paint mTextPaint = new Paint(); + private Rect mTempRect = new Rect(); + + private int mNumRows = 5; + private int mSelectedRow = 0; + private final int mEstimatedPixelHeight = 10; + + private Integer mDesiredHeight = null; + private String mLabel = null; + + public InternalSelectionView(Context context, int numRows, String label) { + super(context); + mNumRows = numRows; + mLabel = label; + init(); + } + + public InternalSelectionView(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = + context.obtainStyledAttributes( + attrs, R.styleable.SelectableRowView); + mNumRows = a.getInt(R.styleable.SelectableRowView_numRows, 5); + init(); + } + + private void init() { + setFocusable(true); + mTextPaint.setAntiAlias(true); + mTextPaint.setTextSize(10); + mTextPaint.setColor(Color.WHITE); + } + + public int getNumRows() { + return mNumRows; + } + + public int getSelectedRow() { + return mSelectedRow; + } + + public void setDesiredHeight(int desiredHeight) { + mDesiredHeight = desiredHeight; + } + + public String getLabel() { + return mLabel; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension( + measureWidth(widthMeasureSpec), + measureHeight(heightMeasureSpec)); + } + + private int measureWidth(int measureSpec) { + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + int desiredWidth = 300 + mPaddingLeft + mPaddingRight; + if (specMode == MeasureSpec.EXACTLY) { + // We were told how big to be + return specSize; + } else if (specMode == MeasureSpec.AT_MOST) { + return desiredWidth < specSize ? desiredWidth : specSize; + } else { + return desiredWidth; + } + } + + private int measureHeight(int measureSpec) { + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + int desiredHeight = mDesiredHeight != null ? + mDesiredHeight : + mNumRows * mEstimatedPixelHeight + mPaddingTop + mPaddingBottom; + if (specMode == MeasureSpec.EXACTLY) { + // We were told how big to be + return specSize; + } else if (specMode == MeasureSpec.AT_MOST) { + return desiredHeight < specSize ? desiredHeight : specSize; + } else { + return desiredHeight; + } + } + + + @Override + protected void onDraw(Canvas canvas) { + int rectTop = mPaddingTop; + int rectLeft = mPaddingLeft; + int rectRight = getWidth() - mPaddingRight; + for (int i = 0; i < mNumRows; i++) { + + mPainter.setColor(Color.BLACK); + mPainter.setAlpha(0x20); + + int rowHeight = getRowHeight(i); + + // draw background rect + mTempRect.set(rectLeft, rectTop, rectRight, rectTop + rowHeight); + canvas.drawRect(mTempRect, mPainter); + + // draw forground rect + if (i == mSelectedRow && hasFocus()) { + mPainter.setColor(Color.RED); + mPainter.setAlpha(0xF0); + mTextPaint.setAlpha(0xFF); + } else { + mPainter.setColor(Color.BLACK); + mPainter.setAlpha(0x40); + mTextPaint.setAlpha(0xF0); + } + mTempRect.set(rectLeft + 2, rectTop + 2, + rectRight - 2, rectTop + rowHeight - 2); + canvas.drawRect(mTempRect, mPainter); + + // draw text to help when visually inspecting + canvas.drawText( + Integer.toString(i), + rectLeft + 2, + rectTop + 2 - (int) mTextPaint.ascent(), + mTextPaint); + + rectTop += rowHeight; + } + } + + private int getRowHeight(int row) { + final int availableHeight = getHeight() - mPaddingTop - mPaddingBottom; + final int desiredRowHeight = availableHeight / mNumRows; + if (row < mNumRows - 1) { + return desiredRowHeight; + } else { + final int residualHeight = availableHeight % mNumRows; + return desiredRowHeight + residualHeight; + } + } + + public void getRectForRow(Rect rect, int row) { + final int rowHeight = getRowHeight(row); + final int top = mPaddingTop + row * rowHeight; + rect.set(mPaddingLeft, + top, + getWidth() - mPaddingRight, + top + rowHeight); + } + + + void ensureRectVisible() { + getRectForRow(mTempRect, mSelectedRow); + requestRectangleOnScreen(mTempRect); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch(event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (mSelectedRow > 0) { + mSelectedRow--; + invalidate(); + ensureRectVisible(); + return true; + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (mSelectedRow < (mNumRows - 1)) { + mSelectedRow++; + invalidate(); + ensureRectVisible(); + return true; + } + break; + } + return false; + } + + + @Override + public void getFocusedRect(Rect r) { + getRectForRow(r, mSelectedRow); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, + Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + + if (focused) { + switch (direction) { + case View.FOCUS_DOWN: + mSelectedRow = 0; + break; + case View.FOCUS_UP: + mSelectedRow = mNumRows - 1; + break; + case View.FOCUS_LEFT: // fall through + case View.FOCUS_RIGHT: + // set the row that is closest to the rect + if (previouslyFocusedRect != null) { + int y = previouslyFocusedRect.top + + (previouslyFocusedRect.height() / 2); + int yPerRow = getHeight() / mNumRows; + mSelectedRow = y / yPerRow; + } else { + mSelectedRow = 0; + } + break; + default: + // can't gleam any useful information about what internal + // selection should be... + return; + } + invalidate(); + } + } + + @Override + public String toString() { + if (mLabel != null) { + return mLabel; + } + return super.toString(); + } +} diff --git a/src/main/java/android/util/JsonReader.java b/src/main/java/android/util/JsonReader.java new file mode 100644 index 0000000..7d1c6c4 --- /dev/null +++ b/src/main/java/android/util/JsonReader.java @@ -0,0 +1,1171 @@ +/* + * Copyright (C) 2010 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 android.util; + +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import libcore.internal.StringPool; + +/** + * Reads a JSON (RFC 4627) + * encoded value as a stream of tokens. This stream includes both literal + * values (strings, numbers, booleans, and nulls) as well as the begin and + * end delimiters of objects and arrays. The tokens are traversed in + * depth-first order, the same order that they appear in the JSON document. + * Within JSON objects, name/value pairs are represented by a single token. + * + *

    Parsing JSON

    + * To create a recursive descent parser for your own JSON streams, first create + * an entry point method that creates a {@code JsonReader}. + * + *

    Next, create handler methods for each structure in your JSON text. You'll + * need a method for each object type and for each array type. + *

      + *
    • Within array handling methods, first call {@link + * #beginArray} to consume the array's opening bracket. Then create a + * while loop that accumulates values, terminating when {@link #hasNext} + * is false. Finally, read the array's closing bracket by calling {@link + * #endArray}. + *
    • Within object handling methods, first call {@link + * #beginObject} to consume the object's opening brace. Then create a + * while loop that assigns values to local variables based on their name. + * This loop should terminate when {@link #hasNext} is false. Finally, + * read the object's closing brace by calling {@link #endObject}. + *
    + *

    When a nested object or array is encountered, delegate to the + * corresponding handler method. + * + *

    When an unknown name is encountered, strict parsers should fail with an + * exception. Lenient parsers should call {@link #skipValue()} to recursively + * skip the value's nested tokens, which may otherwise conflict. + * + *

    If a value may be null, you should first check using {@link #peek()}. + * Null literals can be consumed using either {@link #nextNull()} or {@link + * #skipValue()}. + * + *

    Example

    + * Suppose we'd like to parse a stream of messages such as the following:
     {@code
    + * [
    + *   {
    + *     "id": 912345678901,
    + *     "text": "How do I read JSON on Android?",
    + *     "geo": null,
    + *     "user": {
    + *       "name": "android_newb",
    + *       "followers_count": 41
    + *      }
    + *   },
    + *   {
    + *     "id": 912345678902,
    + *     "text": "@android_newb just use android.util.JsonReader!",
    + *     "geo": [50.454722, -104.606667],
    + *     "user": {
    + *       "name": "jesse",
    + *       "followers_count": 2
    + *     }
    + *   }
    + * ]}
    + * This code implements the parser for the above structure:
       {@code
    + *
    + *   public List readJsonStream(InputStream in) throws IOException {
    + *     JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
    + *     try {
    + *       return readMessagesArray(reader);
    + *     } finally {
    + *       reader.close();
    + *     }
    + *   }
    + *
    + *   public List readMessagesArray(JsonReader reader) throws IOException {
    + *     List messages = new ArrayList();
    + *
    + *     reader.beginArray();
    + *     while (reader.hasNext()) {
    + *       messages.add(readMessage(reader));
    + *     }
    + *     reader.endArray();
    + *     return messages;
    + *   }
    + *
    + *   public Message readMessage(JsonReader reader) throws IOException {
    + *     long id = -1;
    + *     String text = null;
    + *     User user = null;
    + *     List geo = null;
    + *
    + *     reader.beginObject();
    + *     while (reader.hasNext()) {
    + *       String name = reader.nextName();
    + *       if (name.equals("id")) {
    + *         id = reader.nextLong();
    + *       } else if (name.equals("text")) {
    + *         text = reader.nextString();
    + *       } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
    + *         geo = readDoublesArray(reader);
    + *       } else if (name.equals("user")) {
    + *         user = readUser(reader);
    + *       } else {
    + *         reader.skipValue();
    + *       }
    + *     }
    + *     reader.endObject();
    + *     return new Message(id, text, user, geo);
    + *   }
    + *
    + *   public List readDoublesArray(JsonReader reader) throws IOException {
    + *     List doubles = new ArrayList();
    + *
    + *     reader.beginArray();
    + *     while (reader.hasNext()) {
    + *       doubles.add(reader.nextDouble());
    + *     }
    + *     reader.endArray();
    + *     return doubles;
    + *   }
    + *
    + *   public User readUser(JsonReader reader) throws IOException {
    + *     String username = null;
    + *     int followersCount = -1;
    + *
    + *     reader.beginObject();
    + *     while (reader.hasNext()) {
    + *       String name = reader.nextName();
    + *       if (name.equals("name")) {
    + *         username = reader.nextString();
    + *       } else if (name.equals("followers_count")) {
    + *         followersCount = reader.nextInt();
    + *       } else {
    + *         reader.skipValue();
    + *       }
    + *     }
    + *     reader.endObject();
    + *     return new User(username, followersCount);
    + *   }}
    + * + *

    Number Handling

    + * This reader permits numeric values to be read as strings and string values to + * be read as numbers. For example, both elements of the JSON array {@code + * [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}. + * This behavior is intended to prevent lossy numeric conversions: double is + * JavaScript's only numeric type and very large values like {@code + * 9007199254740993} cannot be represented exactly on that platform. To minimize + * precision loss, extremely large values should be written and read as strings + * in JSON. + * + *

    Each {@code JsonReader} may be used to read a single JSON stream. Instances + * of this class are not thread safe. + */ +public final class JsonReader implements Closeable { + + private static final String TRUE = "true"; + private static final String FALSE = "false"; + + private final StringPool stringPool = new StringPool(); + + /** The input JSON. */ + private final Reader in; + + /** True to accept non-spec compliant JSON */ + private boolean lenient = false; + + /** + * Use a manual buffer to easily read and unread upcoming characters, and + * also so we can create strings without an intermediate StringBuilder. + * We decode literals directly out of this buffer, so it must be at least as + * long as the longest token that can be reported as a number. + */ + private final char[] buffer = new char[1024]; + private int pos = 0; + private int limit = 0; + + /* + * The offset of the first character in the buffer. + */ + private int bufferStartLine = 1; + private int bufferStartColumn = 1; + + private final List stack = new ArrayList(); + { + push(JsonScope.EMPTY_DOCUMENT); + } + + /** + * The type of the next token to be returned by {@link #peek} and {@link + * #advance}. If null, peek() will assign a value. + */ + private JsonToken token; + + /** The text of the next name. */ + private String name; + + /* + * For the next literal value, we may have the text value, or the position + * and length in the buffer. + */ + private String value; + private int valuePos; + private int valueLength; + + /** True if we're currently handling a skipValue() call. */ + private boolean skipping = false; + + /** + * Creates a new instance that reads a JSON-encoded stream from {@code in}. + */ + public JsonReader(Reader in) { + if (in == null) { + throw new NullPointerException("in == null"); + } + this.in = in; + } + + /** + * Configure this parser to be be liberal in what it accepts. By default, + * this parser is strict and only accepts JSON as specified by RFC 4627. Setting the + * parser to lenient causes it to ignore the following syntax errors: + * + *

      + *
    • End of line comments starting with {@code //} or {@code #} and + * ending with a newline character. + *
    • C-style comments starting with {@code /*} and ending with + * {@code *}{@code /}. Such comments may not be nested. + *
    • Names that are unquoted or {@code 'single quoted'}. + *
    • Strings that are unquoted or {@code 'single quoted'}. + *
    • Array elements separated by {@code ;} instead of {@code ,}. + *
    • Unnecessary array separators. These are interpreted as if null + * was the omitted value. + *
    • Names and values separated by {@code =} or {@code =>} instead of + * {@code :}. + *
    • Name/value pairs separated by {@code ;} instead of {@code ,}. + *
    + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + /** + * Returns true if this parser is liberal in what it accepts. + */ + public boolean isLenient() { + return lenient; + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new array. + */ + public void beginArray() throws IOException { + expect(JsonToken.BEGIN_ARRAY); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current array. + */ + public void endArray() throws IOException { + expect(JsonToken.END_ARRAY); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * beginning of a new object. + */ + public void beginObject() throws IOException { + expect(JsonToken.BEGIN_OBJECT); + } + + /** + * Consumes the next token from the JSON stream and asserts that it is the + * end of the current array. + */ + public void endObject() throws IOException { + expect(JsonToken.END_OBJECT); + } + + /** + * Consumes {@code expected}. + */ + private void expect(JsonToken expected) throws IOException { + peek(); + if (token != expected) { + throw new IllegalStateException("Expected " + expected + " but was " + peek()); + } + advance(); + } + + /** + * Returns true if the current array or object has another element. + */ + public boolean hasNext() throws IOException { + peek(); + return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY; + } + + /** + * Returns the type of the next token without consuming it. + */ + public JsonToken peek() throws IOException { + if (token != null) { + return token; + } + + switch (peekStack()) { + case EMPTY_DOCUMENT: + replaceTop(JsonScope.NONEMPTY_DOCUMENT); + JsonToken firstToken = nextValue(); + if (!lenient && token != JsonToken.BEGIN_ARRAY && token != JsonToken.BEGIN_OBJECT) { + throw new IOException( + "Expected JSON document to start with '[' or '{' but was " + token); + } + return firstToken; + case EMPTY_ARRAY: + return nextInArray(true); + case NONEMPTY_ARRAY: + return nextInArray(false); + case EMPTY_OBJECT: + return nextInObject(true); + case DANGLING_NAME: + return objectValue(); + case NONEMPTY_OBJECT: + return nextInObject(false); + case NONEMPTY_DOCUMENT: + try { + JsonToken token = nextValue(); + if (lenient) { + return token; + } + throw syntaxError("Expected EOF"); + } catch (EOFException e) { + return token = JsonToken.END_DOCUMENT; // TODO: avoid throwing here? + } + case CLOSED: + throw new IllegalStateException("JsonReader is closed"); + default: + throw new AssertionError(); + } + } + + /** + * Advances the cursor in the JSON stream to the next token. + */ + private JsonToken advance() throws IOException { + peek(); + + JsonToken result = token; + token = null; + value = null; + name = null; + return result; + } + + /** + * Returns the next token, a {@link JsonToken#NAME property name}, and + * consumes it. + * + * @throws IOException if the next token in the stream is not a property + * name. + */ + public String nextName() throws IOException { + peek(); + if (token != JsonToken.NAME) { + throw new IllegalStateException("Expected a name but was " + peek()); + } + String result = name; + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#STRING string} value of the next token, + * consuming it. If the next token is a number, this method will return its + * string form. + * + * @throws IllegalStateException if the next token is not a string or if + * this reader is closed. + */ + public String nextString() throws IOException { + peek(); + if (token != JsonToken.STRING && token != JsonToken.NUMBER) { + throw new IllegalStateException("Expected a string but was " + peek()); + } + + String result = value; + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token, + * consuming it. + * + * @throws IllegalStateException if the next token is not a boolean or if + * this reader is closed. + */ + public boolean nextBoolean() throws IOException { + peek(); + if (token != JsonToken.BOOLEAN) { + throw new IllegalStateException("Expected a boolean but was " + token); + } + + boolean result = (value == TRUE); + advance(); + return result; + } + + /** + * Consumes the next token from the JSON stream and asserts that it is a + * literal null. + * + * @throws IllegalStateException if the next token is not null or if this + * reader is closed. + */ + public void nextNull() throws IOException { + peek(); + if (token != JsonToken.NULL) { + throw new IllegalStateException("Expected null but was " + token); + } + + advance(); + } + + /** + * Returns the {@link JsonToken#NUMBER double} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as a double using {@link Double#parseDouble(String)}. + * + * @throws IllegalStateException if the next token is not a literal value. + */ + public double nextDouble() throws IOException { + peek(); + if (token != JsonToken.STRING && token != JsonToken.NUMBER) { + throw new IllegalStateException("Expected a double but was " + token); + } + + double result = Double.parseDouble(value); + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#NUMBER long} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as a long. If the next token's numeric value cannot be exactly + * represented by a Java {@code long}, this method throws. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a number, or exactly represented as a long. + */ + public long nextLong() throws IOException { + peek(); + if (token != JsonToken.STRING && token != JsonToken.NUMBER) { + throw new IllegalStateException("Expected a long but was " + token); + } + + long result; + try { + result = Long.parseLong(value); + } catch (NumberFormatException ignored) { + double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException + result = (long) asDouble; + if ((double) result != asDouble) { + throw new NumberFormatException(value); + } + } + + advance(); + return result; + } + + /** + * Returns the {@link JsonToken#NUMBER int} value of the next token, + * consuming it. If the next token is a string, this method will attempt to + * parse it as an int. If the next token's numeric value cannot be exactly + * represented by a Java {@code int}, this method throws. + * + * @throws IllegalStateException if the next token is not a literal value. + * @throws NumberFormatException if the next literal value cannot be parsed + * as a number, or exactly represented as an int. + */ + public int nextInt() throws IOException { + peek(); + if (token != JsonToken.STRING && token != JsonToken.NUMBER) { + throw new IllegalStateException("Expected an int but was " + token); + } + + int result; + try { + result = Integer.parseInt(value); + } catch (NumberFormatException ignored) { + double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException + result = (int) asDouble; + if ((double) result != asDouble) { + throw new NumberFormatException(value); + } + } + + advance(); + return result; + } + + /** + * Closes this JSON reader and the underlying {@link Reader}. + */ + public void close() throws IOException { + value = null; + token = null; + stack.clear(); + stack.add(JsonScope.CLOSED); + in.close(); + } + + /** + * Skips the next value recursively. If it is an object or array, all nested + * elements are skipped. This method is intended for use when the JSON token + * stream contains unrecognized or unhandled values. + */ + public void skipValue() throws IOException { + skipping = true; + try { + if (!hasNext() || peek() == JsonToken.END_DOCUMENT) { + throw new IllegalStateException("No element left to skip"); + } + int count = 0; + do { + JsonToken token = advance(); + if (token == JsonToken.BEGIN_ARRAY || token == JsonToken.BEGIN_OBJECT) { + count++; + } else if (token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT) { + count--; + } + } while (count != 0); + } finally { + skipping = false; + } + } + + private JsonScope peekStack() { + return stack.get(stack.size() - 1); + } + + private JsonScope pop() { + return stack.remove(stack.size() - 1); + } + + private void push(JsonScope newTop) { + stack.add(newTop); + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(JsonScope newTop) { + stack.set(stack.size() - 1, newTop); + } + + private JsonToken nextInArray(boolean firstElement) throws IOException { + if (firstElement) { + replaceTop(JsonScope.NONEMPTY_ARRAY); + } else { + /* Look for a comma before each element after the first element. */ + switch (nextNonWhitespace()) { + case ']': + pop(); + return token = JsonToken.END_ARRAY; + case ';': + checkLenient(); // fall-through + case ',': + break; + default: + throw syntaxError("Unterminated array"); + } + } + + switch (nextNonWhitespace()) { + case ']': + if (firstElement) { + pop(); + return token = JsonToken.END_ARRAY; + } + // fall-through to handle ",]" + case ';': + case ',': + /* In lenient mode, a 0-length literal means 'null' */ + checkLenient(); + pos--; + value = "null"; + return token = JsonToken.NULL; + default: + pos--; + return nextValue(); + } + } + + private JsonToken nextInObject(boolean firstElement) throws IOException { + /* + * Read delimiters. Either a comma/semicolon separating this and the + * previous name-value pair, or a close brace to denote the end of the + * object. + */ + if (firstElement) { + /* Peek to see if this is the empty object. */ + switch (nextNonWhitespace()) { + case '}': + pop(); + return token = JsonToken.END_OBJECT; + default: + pos--; + } + } else { + switch (nextNonWhitespace()) { + case '}': + pop(); + return token = JsonToken.END_OBJECT; + case ';': + case ',': + break; + default: + throw syntaxError("Unterminated object"); + } + } + + /* Read the name. */ + int quote = nextNonWhitespace(); + switch (quote) { + case '\'': + checkLenient(); // fall-through + case '"': + name = nextString((char) quote); + break; + default: + checkLenient(); + pos--; + name = nextLiteral(false); + if (name.isEmpty()) { + throw syntaxError("Expected name"); + } + } + + replaceTop(JsonScope.DANGLING_NAME); + return token = JsonToken.NAME; + } + + private JsonToken objectValue() throws IOException { + /* + * Read the name/value separator. Usually a colon ':'. In lenient mode + * we also accept an equals sign '=', or an arrow "=>". + */ + switch (nextNonWhitespace()) { + case ':': + break; + case '=': + checkLenient(); + if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') { + pos++; + } + break; + default: + throw syntaxError("Expected ':'"); + } + + replaceTop(JsonScope.NONEMPTY_OBJECT); + return nextValue(); + } + + private JsonToken nextValue() throws IOException { + int c = nextNonWhitespace(); + switch (c) { + case '{': + push(JsonScope.EMPTY_OBJECT); + return token = JsonToken.BEGIN_OBJECT; + + case '[': + push(JsonScope.EMPTY_ARRAY); + return token = JsonToken.BEGIN_ARRAY; + + case '\'': + checkLenient(); // fall-through + case '"': + value = nextString((char) c); + return token = JsonToken.STRING; + + default: + pos--; + return readLiteral(); + } + } + + /** + * Returns true once {@code limit - pos >= minimum}. If the data is + * exhausted before that many characters are available, this returns + * false. + */ + private boolean fillBuffer(int minimum) throws IOException { + // Before clobbering the old characters, update where buffer starts + for (int i = 0; i < pos; i++) { + if (buffer[i] == '\n') { + bufferStartLine++; + bufferStartColumn = 1; + } else { + bufferStartColumn++; + } + } + + if (limit != pos) { + limit -= pos; + System.arraycopy(buffer, pos, buffer, 0, limit); + } else { + limit = 0; + } + + pos = 0; + int total; + while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) { + limit += total; + + // if this is the first read, consume an optional byte order mark (BOM) if it exists + if (bufferStartLine == 1 && bufferStartColumn == 1 + && limit > 0 && buffer[0] == '\ufeff') { + pos++; + bufferStartColumn--; + } + + if (limit >= minimum) { + return true; + } + } + return false; + } + + private int getLineNumber() { + int result = bufferStartLine; + for (int i = 0; i < pos; i++) { + if (buffer[i] == '\n') { + result++; + } + } + return result; + } + + private int getColumnNumber() { + int result = bufferStartColumn; + for (int i = 0; i < pos; i++) { + if (buffer[i] == '\n') { + result = 1; + } else { + result++; + } + } + return result; + } + + private int nextNonWhitespace() throws IOException { + while (pos < limit || fillBuffer(1)) { + int c = buffer[pos++]; + switch (c) { + case '\t': + case ' ': + case '\n': + case '\r': + continue; + + case '/': + if (pos == limit && !fillBuffer(1)) { + return c; + } + + checkLenient(); + char peek = buffer[pos]; + switch (peek) { + case '*': + // skip a /* c-style comment */ + pos++; + if (!skipTo("*/")) { + throw syntaxError("Unterminated comment"); + } + pos += 2; + continue; + + case '/': + // skip a // end-of-line comment + pos++; + skipToEndOfLine(); + continue; + + default: + return c; + } + + case '#': + /* + * Skip a # hash end-of-line comment. The JSON RFC doesn't + * specify this behaviour, but it's required to parse + * existing documents. See http://b/2571423. + */ + checkLenient(); + skipToEndOfLine(); + continue; + + default: + return c; + } + } + + throw new EOFException("End of input"); + } + + private void checkLenient() throws IOException { + if (!lenient) { + throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON"); + } + } + + /** + * Advances the position until after the next newline character. If the line + * is terminated by "\r\n", the '\n' must be consumed as whitespace by the + * caller. + */ + private void skipToEndOfLine() throws IOException { + while (pos < limit || fillBuffer(1)) { + char c = buffer[pos++]; + if (c == '\r' || c == '\n') { + break; + } + } + } + + private boolean skipTo(String toFind) throws IOException { + outer: + for (; pos + toFind.length() <= limit || fillBuffer(toFind.length()); pos++) { + for (int c = 0; c < toFind.length(); c++) { + if (buffer[pos + c] != toFind.charAt(c)) { + continue outer; + } + } + return true; + } + return false; + } + + /** + * Returns the string up to but not including {@code quote}, unescaping any + * character escape sequences encountered along the way. The opening quote + * should have already been read. This consumes the closing quote, but does + * not include it in the returned string. + * + * @param quote either ' or ". + * @throws NumberFormatException if any unicode escape sequences are + * malformed. + */ + private String nextString(char quote) throws IOException { + StringBuilder builder = null; + do { + /* the index of the first character not yet appended to the builder. */ + int start = pos; + while (pos < limit) { + int c = buffer[pos++]; + + if (c == quote) { + if (skipping) { + return "skipped!"; + } else if (builder == null) { + return stringPool.get(buffer, start, pos - start - 1); + } else { + builder.append(buffer, start, pos - start - 1); + return builder.toString(); + } + + } else if (c == '\\') { + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start - 1); + builder.append(readEscapeCharacter()); + start = pos; + } + } + + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, start, pos - start); + } while (fillBuffer(1)); + + throw syntaxError("Unterminated string"); + } + + /** + * Reads the value up to but not including any delimiter characters. This + * does not consume the delimiter character. + * + * @param assignOffsetsOnly true for this method to only set the valuePos + * and valueLength fields and return a null result. This only works if + * the literal is short; a string is returned otherwise. + */ + private String nextLiteral(boolean assignOffsetsOnly) throws IOException { + StringBuilder builder = null; + valuePos = -1; + valueLength = 0; + int i = 0; + + findNonLiteralCharacter: + while (true) { + for (; pos + i < limit; i++) { + switch (buffer[pos + i]) { + case '/': + case '\\': + case ';': + case '#': + case '=': + checkLenient(); // fall-through + case '{': + case '}': + case '[': + case ']': + case ':': + case ',': + case ' ': + case '\t': + case '\f': + case '\r': + case '\n': + break findNonLiteralCharacter; + } + } + + /* + * Attempt to load the entire literal into the buffer at once. If + * we run out of input, add a non-literal character at the end so + * that decoding doesn't need to do bounds checks. + */ + if (i < buffer.length) { + if (fillBuffer(i + 1)) { + continue; + } else { + buffer[limit] = '\0'; + break; + } + } + + // use a StringBuilder when the value is too long. It must be an unquoted string. + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(buffer, pos, i); + valueLength += i; + pos += i; + i = 0; + if (!fillBuffer(1)) { + break; + } + } + + String result; + if (assignOffsetsOnly && builder == null) { + valuePos = pos; + result = null; + } else if (skipping) { + result = "skipped!"; + } else if (builder == null) { + result = stringPool.get(buffer, pos, i); + } else { + builder.append(buffer, pos, i); + result = builder.toString(); + } + valueLength += i; + pos += i; + return result; + } + + @Override public String toString() { + return getClass().getSimpleName() + " near " + getSnippet(); + } + + /** + * Unescapes the character identified by the character or characters that + * immediately follow a backslash. The backslash '\' should have already + * been read. This supports both unicode escapes "u000A" and two-character + * escapes "\n". + * + * @throws NumberFormatException if any unicode escape sequences are + * malformed. + */ + private char readEscapeCharacter() throws IOException { + if (pos == limit && !fillBuffer(1)) { + throw syntaxError("Unterminated escape sequence"); + } + + char escaped = buffer[pos++]; + switch (escaped) { + case 'u': + if (pos + 4 > limit && !fillBuffer(4)) { + throw syntaxError("Unterminated escape sequence"); + } + String hex = stringPool.get(buffer, pos, 4); + pos += 4; + return (char) Integer.parseInt(hex, 16); + + case 't': + return '\t'; + + case 'b': + return '\b'; + + case 'n': + return '\n'; + + case 'r': + return '\r'; + + case 'f': + return '\f'; + + case '\'': + case '"': + case '\\': + default: + return escaped; + } + } + + /** + * Reads a null, boolean, numeric or unquoted string literal value. + */ + private JsonToken readLiteral() throws IOException { + value = nextLiteral(true); + if (valueLength == 0) { + throw syntaxError("Expected literal value"); + } + token = decodeLiteral(); + if (token == JsonToken.STRING) { + checkLenient(); + } + return token; + } + + /** + * Assigns {@code nextToken} based on the value of {@code nextValue}. + */ + private JsonToken decodeLiteral() throws IOException { + if (valuePos == -1) { + // it was too long to fit in the buffer so it can only be a string + return JsonToken.STRING; + } else if (valueLength == 4 + && ('n' == buffer[valuePos ] || 'N' == buffer[valuePos ]) + && ('u' == buffer[valuePos + 1] || 'U' == buffer[valuePos + 1]) + && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2]) + && ('l' == buffer[valuePos + 3] || 'L' == buffer[valuePos + 3])) { + value = "null"; + return JsonToken.NULL; + } else if (valueLength == 4 + && ('t' == buffer[valuePos ] || 'T' == buffer[valuePos ]) + && ('r' == buffer[valuePos + 1] || 'R' == buffer[valuePos + 1]) + && ('u' == buffer[valuePos + 2] || 'U' == buffer[valuePos + 2]) + && ('e' == buffer[valuePos + 3] || 'E' == buffer[valuePos + 3])) { + value = TRUE; + return JsonToken.BOOLEAN; + } else if (valueLength == 5 + && ('f' == buffer[valuePos ] || 'F' == buffer[valuePos ]) + && ('a' == buffer[valuePos + 1] || 'A' == buffer[valuePos + 1]) + && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2]) + && ('s' == buffer[valuePos + 3] || 'S' == buffer[valuePos + 3]) + && ('e' == buffer[valuePos + 4] || 'E' == buffer[valuePos + 4])) { + value = FALSE; + return JsonToken.BOOLEAN; + } else { + value = stringPool.get(buffer, valuePos, valueLength); + return decodeNumber(buffer, valuePos, valueLength); + } + } + + /** + * Determine whether the characters is a JSON number. Numbers are of the + * form -12.34e+56. Fractional and exponential parts are optional. Leading + * zeroes are not allowed in the value or exponential part, but are allowed + * in the fraction. + */ + private JsonToken decodeNumber(char[] chars, int offset, int length) { + int i = offset; + int c = chars[i]; + + if (c == '-') { + c = chars[++i]; + } + + if (c == '0') { + c = chars[++i]; + } else if (c >= '1' && c <= '9') { + c = chars[++i]; + while (c >= '0' && c <= '9') { + c = chars[++i]; + } + } else { + return JsonToken.STRING; + } + + if (c == '.') { + c = chars[++i]; + while (c >= '0' && c <= '9') { + c = chars[++i]; + } + } + + if (c == 'e' || c == 'E') { + c = chars[++i]; + if (c == '+' || c == '-') { + c = chars[++i]; + } + if (c >= '0' && c <= '9') { + c = chars[++i]; + while (c >= '0' && c <= '9') { + c = chars[++i]; + } + } else { + return JsonToken.STRING; + } + } + + if (i == offset + length) { + return JsonToken.NUMBER; + } else { + return JsonToken.STRING; + } + } + + /** + * Throws a new IO exception with the given message and a context snippet + * with this reader's content. + */ + private IOException syntaxError(String message) throws IOException { + throw new MalformedJsonException(message + + " at line " + getLineNumber() + " column " + getColumnNumber()); + } + + private CharSequence getSnippet() { + StringBuilder snippet = new StringBuilder(); + int beforePos = Math.min(pos, 20); + snippet.append(buffer, pos - beforePos, beforePos); + int afterPos = Math.min(limit - pos, 20); + snippet.append(buffer, pos, afterPos); + return snippet; + } +} diff --git a/src/main/java/android/util/JsonScope.java b/src/main/java/android/util/JsonScope.java new file mode 100644 index 0000000..ca534e9 --- /dev/null +++ b/src/main/java/android/util/JsonScope.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 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 android.util; + +/** + * Lexical scoping elements within a JSON reader or writer. + */ +enum JsonScope { + + /** + * An array with no elements requires no separators or newlines before + * it is closed. + */ + EMPTY_ARRAY, + + /** + * A array with at least one value requires a comma and newline before + * the next element. + */ + NONEMPTY_ARRAY, + + /** + * An object with no name/value pairs requires no separators or newlines + * before it is closed. + */ + EMPTY_OBJECT, + + /** + * An object whose most recent element is a key. The next element must + * be a value. + */ + DANGLING_NAME, + + /** + * An object with at least one name/value pair requires a comma and + * newline before the next element. + */ + NONEMPTY_OBJECT, + + /** + * No object or array has been started. + */ + EMPTY_DOCUMENT, + + /** + * A document with at an array or object. + */ + NONEMPTY_DOCUMENT, + + /** + * A document that's been closed and cannot be accessed. + */ + CLOSED, +} diff --git a/src/main/java/android/util/JsonToken.java b/src/main/java/android/util/JsonToken.java new file mode 100644 index 0000000..45bc6ca --- /dev/null +++ b/src/main/java/android/util/JsonToken.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 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 android.util; + +/** + * A structure, name or value type in a JSON-encoded string. + */ +public enum JsonToken { + + /** + * The opening of a JSON array. Written using {@link JsonWriter#beginObject} + * and read using {@link JsonReader#beginObject}. + */ + BEGIN_ARRAY, + + /** + * The closing of a JSON array. Written using {@link JsonWriter#endArray} + * and read using {@link JsonReader#endArray}. + */ + END_ARRAY, + + /** + * The opening of a JSON object. Written using {@link JsonWriter#beginObject} + * and read using {@link JsonReader#beginObject}. + */ + BEGIN_OBJECT, + + /** + * The closing of a JSON object. Written using {@link JsonWriter#endObject} + * and read using {@link JsonReader#endObject}. + */ + END_OBJECT, + + /** + * A JSON property name. Within objects, tokens alternate between names and + * their values. Written using {@link JsonWriter#name} and read using {@link + * JsonReader#nextName} + */ + NAME, + + /** + * A JSON string. + */ + STRING, + + /** + * A JSON number represented in this API by a Java {@code double}, {@code + * long}, or {@code int}. + */ + NUMBER, + + /** + * A JSON {@code true} or {@code false}. + */ + BOOLEAN, + + /** + * A JSON {@code null}. + */ + NULL, + + /** + * The end of the JSON stream. This sentinel value is returned by {@link + * JsonReader#peek()} to signal that the JSON-encoded value has no more + * tokens. + */ + END_DOCUMENT +} diff --git a/src/main/java/android/util/JsonWriter.java b/src/main/java/android/util/JsonWriter.java new file mode 100644 index 0000000..c1e6e40 --- /dev/null +++ b/src/main/java/android/util/JsonWriter.java @@ -0,0 +1,528 @@ +/* + * Copyright (C) 2010 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 android.util; + +import java.io.Closeable; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +/** + * Writes a JSON (RFC 4627) + * encoded value to a stream, one token at a time. The stream includes both + * literal values (strings, numbers, booleans and nulls) as well as the begin + * and end delimiters of objects and arrays. + * + *

    Encoding JSON

    + * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON + * document must contain one top-level array or object. Call methods on the + * writer as you walk the structure's contents, nesting arrays and objects as + * necessary: + *
      + *
    • To write arrays, first call {@link #beginArray()}. + * Write each of the array's elements with the appropriate {@link #value} + * methods or by nesting other arrays and objects. Finally close the array + * using {@link #endArray()}. + *
    • To write objects, first call {@link #beginObject()}. + * Write each of the object's properties by alternating calls to + * {@link #name} with the property's value. Write property values with the + * appropriate {@link #value} method or by nesting other objects or arrays. + * Finally close the object using {@link #endObject()}. + *
    + * + *

    Example

    + * Suppose we'd like to encode a stream of messages such as the following:
     {@code
    + * [
    + *   {
    + *     "id": 912345678901,
    + *     "text": "How do I write JSON on Android?",
    + *     "geo": null,
    + *     "user": {
    + *       "name": "android_newb",
    + *       "followers_count": 41
    + *      }
    + *   },
    + *   {
    + *     "id": 912345678902,
    + *     "text": "@android_newb just use android.util.JsonWriter!",
    + *     "geo": [50.454722, -104.606667],
    + *     "user": {
    + *       "name": "jesse",
    + *       "followers_count": 2
    + *     }
    + *   }
    + * ]}
    + * This code encodes the above structure:
       {@code
    + *   public void writeJsonStream(OutputStream out, List messages) throws IOException {
    + *     JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
    + *     writer.setIndent("  ");
    + *     writeMessagesArray(writer, messages);
    + *     writer.close();
    + *   }
    + *
    + *   public void writeMessagesArray(JsonWriter writer, List messages) throws IOException {
    + *     writer.beginArray();
    + *     for (Message message : messages) {
    + *       writeMessage(writer, message);
    + *     }
    + *     writer.endArray();
    + *   }
    + *
    + *   public void writeMessage(JsonWriter writer, Message message) throws IOException {
    + *     writer.beginObject();
    + *     writer.name("id").value(message.getId());
    + *     writer.name("text").value(message.getText());
    + *     if (message.getGeo() != null) {
    + *       writer.name("geo");
    + *       writeDoublesArray(writer, message.getGeo());
    + *     } else {
    + *       writer.name("geo").nullValue();
    + *     }
    + *     writer.name("user");
    + *     writeUser(writer, message.getUser());
    + *     writer.endObject();
    + *   }
    + *
    + *   public void writeUser(JsonWriter writer, User user) throws IOException {
    + *     writer.beginObject();
    + *     writer.name("name").value(user.getName());
    + *     writer.name("followers_count").value(user.getFollowersCount());
    + *     writer.endObject();
    + *   }
    + *
    + *   public void writeDoublesArray(JsonWriter writer, List doubles) throws IOException {
    + *     writer.beginArray();
    + *     for (Double value : doubles) {
    + *       writer.value(value);
    + *     }
    + *     writer.endArray();
    + *   }}
    + * + *

    Each {@code JsonWriter} may be used to write a single JSON stream. + * Instances of this class are not thread safe. Calls that would result in a + * malformed JSON string will fail with an {@link IllegalStateException}. + */ +public final class JsonWriter implements Closeable { + + /** The output data, containing at most one top-level array or object. */ + private final Writer out; + + private final List stack = new ArrayList(); + { + stack.add(JsonScope.EMPTY_DOCUMENT); + } + + /** + * A string containing a full set of spaces for a single level of + * indentation, or null for no pretty printing. + */ + private String indent; + + /** + * The name/value separator; either ":" or ": ". + */ + private String separator = ":"; + + private boolean lenient; + + /** + * Creates a new instance that writes a JSON-encoded stream to {@code out}. + * For best performance, ensure {@link Writer} is buffered; wrapping in + * {@link java.io.BufferedWriter BufferedWriter} if necessary. + */ + public JsonWriter(Writer out) { + if (out == null) { + throw new NullPointerException("out == null"); + } + this.out = out; + } + + /** + * Sets the indentation string to be repeated for each level of indentation + * in the encoded document. If {@code indent.isEmpty()} the encoded document + * will be compact. Otherwise the encoded document will be more + * human-readable. + * + * @param indent a string containing only whitespace. + */ + public void setIndent(String indent) { + if (indent.isEmpty()) { + this.indent = null; + this.separator = ":"; + } else { + this.indent = indent; + this.separator = ": "; + } + } + + /** + * Configure this writer to relax its syntax rules. By default, this writer + * only emits well-formed JSON as specified by RFC 4627. Setting the writer + * to lenient permits the following: + *

      + *
    • Top-level values of any type. With strict writing, the top-level + * value must be an object or an array. + *
    • Numbers may be {@link Double#isNaN() NaNs} or {@link + * Double#isInfinite() infinities}. + *
    + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + /** + * Returns true if this writer has relaxed syntax rules. + */ + public boolean isLenient() { + return lenient; + } + + /** + * Begins encoding a new array. Each call to this method must be paired with + * a call to {@link #endArray}. + * + * @return this writer. + */ + public JsonWriter beginArray() throws IOException { + return open(JsonScope.EMPTY_ARRAY, "["); + } + + /** + * Ends encoding the current array. + * + * @return this writer. + */ + public JsonWriter endArray() throws IOException { + return close(JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY, "]"); + } + + /** + * Begins encoding a new object. Each call to this method must be paired + * with a call to {@link #endObject}. + * + * @return this writer. + */ + public JsonWriter beginObject() throws IOException { + return open(JsonScope.EMPTY_OBJECT, "{"); + } + + /** + * Ends encoding the current object. + * + * @return this writer. + */ + public JsonWriter endObject() throws IOException { + return close(JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT, "}"); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given + * bracket. + */ + private JsonWriter open(JsonScope empty, String openBracket) throws IOException { + beforeValue(true); + stack.add(empty); + out.write(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the + * given bracket. + */ + private JsonWriter close(JsonScope empty, JsonScope nonempty, String closeBracket) + throws IOException { + JsonScope context = peek(); + if (context != nonempty && context != empty) { + throw new IllegalStateException("Nesting problem: " + stack); + } + + stack.remove(stack.size() - 1); + if (context == nonempty) { + newline(); + } + out.write(closeBracket); + return this; + } + + /** + * Returns the value on the top of the stack. + */ + private JsonScope peek() { + return stack.get(stack.size() - 1); + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(JsonScope topOfStack) { + stack.set(stack.size() - 1, topOfStack); + } + + /** + * Encodes the property name. + * + * @param name the name of the forthcoming value. May not be null. + * @return this writer. + */ + public JsonWriter name(String name) throws IOException { + if (name == null) { + throw new NullPointerException("name == null"); + } + beforeName(); + string(name); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value the literal string value, or null to encode a null literal. + * @return this writer. + */ + public JsonWriter value(String value) throws IOException { + if (value == null) { + return nullValue(); + } + beforeValue(false); + string(value); + return this; + } + + /** + * Encodes {@code null}. + * + * @return this writer. + */ + public JsonWriter nullValue() throws IOException { + beforeValue(false); + out.write("null"); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(boolean value) throws IOException { + beforeValue(false); + out.write(value ? "true" : "false"); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities} unless this writer is lenient. + * @return this writer. + */ + public JsonWriter value(double value) throws IOException { + if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + beforeValue(false); + out.append(Double.toString(value)); + return this; + } + + /** + * Encodes {@code value}. + * + * @return this writer. + */ + public JsonWriter value(long value) throws IOException { + beforeValue(false); + out.write(Long.toString(value)); + return this; + } + + /** + * Encodes {@code value}. + * + * @param value a finite value. May not be {@link Double#isNaN() NaNs} or + * {@link Double#isInfinite() infinities} unless this writer is lenient. + * @return this writer. + */ + public JsonWriter value(Number value) throws IOException { + if (value == null) { + return nullValue(); + } + + String string = value.toString(); + if (!lenient && + (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + beforeValue(false); + out.append(string); + return this; + } + + /** + * Ensures all buffered data is written to the underlying {@link Writer} + * and flushes that writer. + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Flushes and closes this writer and the underlying {@link Writer}. + * + * @throws IOException if the JSON document is incomplete. + */ + public void close() throws IOException { + out.close(); + + if (peek() != JsonScope.NONEMPTY_DOCUMENT) { + throw new IOException("Incomplete document"); + } + } + + private void string(String value) throws IOException { + out.write("\""); + for (int i = 0, length = value.length(); i < length; i++) { + char c = value.charAt(i); + + /* + * From RFC 4627, "All Unicode characters may be placed within the + * quotation marks except for the characters that must be escaped: + * quotation mark, reverse solidus, and the control characters + * (U+0000 through U+001F)." + * + * We also escape '\u2028' and '\u2029', which JavaScript interprets + * as newline characters. This prevents eval() from failing with a + * syntax error. + * http://code.google.com/p/google-gson/issues/detail?id=341 + */ + switch (c) { + case '"': + case '\\': + out.write('\\'); + out.write(c); + break; + + case '\t': + out.write("\\t"); + break; + + case '\b': + out.write("\\b"); + break; + + case '\n': + out.write("\\n"); + break; + + case '\r': + out.write("\\r"); + break; + + case '\f': + out.write("\\f"); + break; + + case '\u2028': + case '\u2029': + out.write(String.format("\\u%04x", (int) c)); + break; + + default: + if (c <= 0x1F) { + out.write(String.format("\\u%04x", (int) c)); + } else { + out.write(c); + } + break; + } + + } + out.write("\""); + } + + private void newline() throws IOException { + if (indent == null) { + return; + } + + out.write("\n"); + for (int i = 1; i < stack.size(); i++) { + out.write(indent); + } + } + + /** + * Inserts any necessary separators and whitespace before a name. Also + * adjusts the stack to expect the name's value. + */ + private void beforeName() throws IOException { + JsonScope context = peek(); + if (context == JsonScope.NONEMPTY_OBJECT) { // first in object + out.write(','); + } else if (context != JsonScope.EMPTY_OBJECT) { // not in an object! + throw new IllegalStateException("Nesting problem: " + stack); + } + newline(); + replaceTop(JsonScope.DANGLING_NAME); + } + + /** + * Inserts any necessary separators and whitespace before a literal value, + * inline array, or inline object. Also adjusts the stack to expect either a + * closing bracket or another element. + * + * @param root true if the value is a new array or object, the two values + * permitted as top-level elements. + */ + private void beforeValue(boolean root) throws IOException { + switch (peek()) { + case EMPTY_DOCUMENT: // first in document + if (!lenient && !root) { + throw new IllegalStateException( + "JSON must start with an array or an object."); + } + replaceTop(JsonScope.NONEMPTY_DOCUMENT); + break; + + case EMPTY_ARRAY: // first in array + replaceTop(JsonScope.NONEMPTY_ARRAY); + newline(); + break; + + case NONEMPTY_ARRAY: // another in array + out.append(','); + newline(); + break; + + case DANGLING_NAME: // value for name + out.append(separator); + replaceTop(JsonScope.NONEMPTY_OBJECT); + break; + + case NONEMPTY_DOCUMENT: + throw new IllegalStateException( + "JSON must have only one top-level value."); + + default: + throw new IllegalStateException("Nesting problem: " + stack); + } + } +} diff --git a/src/main/java/android/util/KeyUtils.java b/src/main/java/android/util/KeyUtils.java new file mode 100644 index 0000000..b58fda3 --- /dev/null +++ b/src/main/java/android/util/KeyUtils.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2007 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 android.util; + +import android.app.Instrumentation; +import android.os.SystemClock; +import android.test.ActivityInstrumentationTestCase; +import android.test.InstrumentationTestCase; +import android.view.Gravity; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; + +/** + * Reusable methods for generating key events. + *

    + * Definitions: + *

  • Tap refers to pushing and releasing a button (down and up event). + *
  • Chord refers to pushing a modifier key, tapping a regular key, and + * releasing the modifier key. + */ +public class KeyUtils { + /** + * Simulates tapping the menu key. + * + * @param test The test case that is being run. + */ + public static void tapMenuKey(ActivityInstrumentationTestCase test) { + final Instrumentation inst = test.getInstrumentation(); + + inst.sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU)); + inst.sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)); + } + + /** + * Simulates chording the menu key. + * + * @param test The test case that is being run. + * @param shortcutKey The shortcut key to tap while chording the menu key. + */ + public static void chordMenuKey(ActivityInstrumentationTestCase test, char shortcutKey) { + final Instrumentation inst = test.getInstrumentation(); + + final KeyEvent pushMenuKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU); + final KeyCharacterMap keyCharMap = KeyCharacterMap.load(pushMenuKey.getDeviceId()); + final KeyEvent shortcutKeyEvent = keyCharMap.getEvents(new char[] { shortcutKey })[0]; + final int shortcutKeyCode = shortcutKeyEvent.getKeyCode(); + + inst.sendKeySync(pushMenuKey); + inst.sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, shortcutKeyCode)); + inst.sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, shortcutKeyCode)); + inst.sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)); + } + + /** + * Simulates a long click via the keyboard. + * + * @param test The test case that is being run. + */ + public static void longClick(ActivityInstrumentationTestCase test) { + final Instrumentation inst = test.getInstrumentation(); + + inst.sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER)); + try { + Thread.sleep((long)(ViewConfiguration.getLongPressTimeout() * 1.5f)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + inst.sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER)); + } +} diff --git a/src/main/java/android/util/LayoutDirection.java b/src/main/java/android/util/LayoutDirection.java new file mode 100644 index 0000000..20af20b --- /dev/null +++ b/src/main/java/android/util/LayoutDirection.java @@ -0,0 +1,48 @@ +/* + * 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 android.util; + +/** + * A class for defining layout directions. A layout direction can be left-to-right (LTR) + * or right-to-left (RTL). It can also be inherited (from a parent) or deduced from the default + * language script of a locale. + */ +public final class LayoutDirection { + + // No instantiation + private LayoutDirection() {} + + /** + * Horizontal layout direction is from Left to Right. + */ + public static final int LTR = 0; + + /** + * Horizontal layout direction is from Right to Left. + */ + public static final int RTL = 1; + + /** + * Horizontal layout direction is inherited. + */ + public static final int INHERIT = 2; + + /** + * Horizontal layout direction is deduced from the default language script for the locale. + */ + public static final int LOCALE = 3; +} diff --git a/src/main/java/android/util/ListItemFactory.java b/src/main/java/android/util/ListItemFactory.java new file mode 100644 index 0000000..3f48dcc --- /dev/null +++ b/src/main/java/android/util/ListItemFactory.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2007 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 android.util; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * Reusable methods for creating more complex list items. + */ +public class ListItemFactory { + + /** + * Create a view with a button at the top and bottom, with filler in between. + * The filler is sized to take up any space left over within desiredHeight. + * + * @param position The position within the list. + * @param context The context. + * @param desiredHeight The desired height of the entire view. + * @return The created view. + */ + public static View twoButtonsSeparatedByFiller(int position, Context context, int desiredHeight) { + if (desiredHeight < 90) { + throw new IllegalArgumentException("need at least 90 pixels of height " + + "to create the two buttons and leave 10 pixels for the filler"); + } + + final LinearLayout ll = new LinearLayout(context); + ll.setOrientation(LinearLayout.VERTICAL); + + final LinearLayout.LayoutParams buttonLp = + new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + 50); + + final Button topButton = new Button(context); + topButton.setLayoutParams( + buttonLp); + topButton.setText("top (position " + position + ")"); + ll.addView(topButton); + + final TextView middleFiller = new TextView(context); + middleFiller.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + desiredHeight - 100)); + middleFiller.setText("filler"); + ll.addView(middleFiller); + + final Button bottomButton = new Button(context); + bottomButton.setLayoutParams(buttonLp); + bottomButton.setText("bottom (position " + position + ")"); + ll.addView(bottomButton); + ll.setTag("twoButtons"); + return ll; + } + + public enum Slot { + Left, + Middle, + Right + } + + /** + * Create a horizontal linear layout divided into thirds (with some margins + * separating the thirds), filled with buttons into some slots. + * @param context The context. + * @param desiredHeight The height of the LL. + * @param slots Which slots to fill with buttons. + * @return The linear layout. + */ + public static View horizontalButtonSlots(Context context, int desiredHeight, Slot... slots) { + + final LinearLayout ll = new LinearLayout(context); + ll.setOrientation(LinearLayout.HORIZONTAL); + + final LinearLayout.LayoutParams lp + = new LinearLayout.LayoutParams(0, desiredHeight); + lp.setMargins(10, 0, 10, 0); + lp.weight = 0.33f; + + boolean left = false; + boolean middle = false; + boolean right = false; + for (Slot slot : slots) { + switch (slot) { + case Left: + left = true; + break; + case Middle: + middle = true; + break; + case Right: + right = true; + break; + } + } + + if (left) { + final Button button = new Button(context); + button.setText("left"); + ll.addView(button, lp); + } else { + ll.addView(new View(context), lp); + } + + if (middle) { + final Button button = new Button(context); + button.setText("center"); + ll.addView(button, lp); + } else { + ll.addView(new View(context), lp); + } + + if (right) { + final Button button = new Button(context); + button.setText("right"); + ll.addView(button, lp); + } else { + ll.addView(new View(context), lp); + } + + return ll; + } + + + /** + * Create a button ready to be a list item. + * + * @param position The position within the list. + * @param context The context. + * @param text The text of the button + * @param desiredHeight The desired height of the button + * @return The created view. + */ + public static View button(int position, Context context, String text, int desiredHeight) { + TextView result = new Button(context); + result.setHeight(desiredHeight); + result.setText(text); + final ViewGroup.LayoutParams lp = new AbsListView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + result.setLayoutParams(lp); + result.setId(position); + result.setTag("button"); + return result; + } + + /** + * Convert an existing button view to display the data at a new position. + * + * @param convertView Non-null Button created by {@link #button} + * @param text The text of the button + * @param position The position withion the list + * @return The converted view + */ + public static View convertButton(View convertView, String text, int position) { + if (((String) convertView.getTag()).equals("button")) { + ((Button) convertView).setText(text); + convertView.setId(position); + return convertView; + } else { + return null; + } + } + + /** + * Create a text view ready to be a list item. + * + * @param position The position within the list. + * @param context The context. + * @param text The text to display + * @param desiredHeight The desired height of the text view + * @return The created view. + */ + public static View text(int position, Context context, String text, int desiredHeight) { + TextView result = new TextView(context); + result.setHeight(desiredHeight); + result.setText(text); + final ViewGroup.LayoutParams lp = new AbsListView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + result.setLayoutParams(lp); + result.setId(position); + result.setTag("text"); + return result; + } + + /** + * Convert an existing text view to display the data at a new position. + * + * @param convertView Non-null TextView created by {@link #text} + * @param text The text to display + * @param position The position withion the list + * @return The converted view + */ + public static View convertText(View convertView, String text, int position) { + if(convertView.getTag() != null && ((String) convertView.getTag()).equals("text")) { + ((TextView) convertView).setText(text); + convertView.setId(position); + return convertView; + + } else { + return null; + } + } + + /** + * Create a text view ready to be a list item. + * + * @param position The position within the list. + * @param context The context. + * @param text The text of the button + * @param desiredHeight The desired height of the button + * @return The created view. + */ + public static View doubleText(int position, Context context, String text, int desiredHeight) { + final LinearLayout ll = new LinearLayout(context); + ll.setOrientation(LinearLayout.HORIZONTAL); + + final AbsListView.LayoutParams lp = + new AbsListView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + desiredHeight); + ll.setLayoutParams(lp); + ll.setId(position); + + TextView t1 = new TextView(context); + t1.setHeight(desiredHeight); + t1.setText(text); + t1.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); + final ViewGroup.LayoutParams lp1 = new LinearLayout.LayoutParams( + 0, + ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f); + ll.addView(t1, lp1); + + TextView t2 = new TextView(context); + t2.setHeight(desiredHeight); + t2.setText(text); + t2.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL); + final ViewGroup.LayoutParams lp2 = new LinearLayout.LayoutParams( + 0, + ViewGroup.LayoutParams.WRAP_CONTENT, + 1.0f); + + ll.addView(t2, lp2); + ll.setTag("double"); + return ll; + } + + + /** + * Convert an existing button view to display the data at a new position. + * + * @param convertView Non-null view created by {@link #doubleText} + * @param text The text of the button + * @param position The position withion the list + * @return The converted view + */ + public static View convertDoubleText(View convertView, String text, int position) { + if (((String) convertView.getTag()).equals("double")) { + TextView t1 = (TextView) ((LinearLayout) convertView).getChildAt(0); + TextView t2 = (TextView) ((LinearLayout) convertView).getChildAt(1); + t1.setText(text); + t2.setText(text); + convertView.setId(position); + return convertView; + } else { + return null; + } + } +} diff --git a/src/main/java/android/util/ListScenario.java b/src/main/java/android/util/ListScenario.java new file mode 100644 index 0000000..fa088a3 --- /dev/null +++ b/src/main/java/android/util/ListScenario.java @@ -0,0 +1,662 @@ +/* + * Copyright (C) 2007 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 android.util; + +import android.app.Activity; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; +import com.google.android.collect.Maps; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Utility base class for creating various List scenarios. Configurable by the number + * of items, how tall each item should be (in relation to the screen height), and + * what item should start with selection. + */ +public abstract class ListScenario extends Activity { + + private ListView mListView; + private TextView mHeaderTextView; + + private int mNumItems; + protected boolean mItemsFocusable; + + private int mStartingSelectionPosition; + private double mItemScreenSizeFactor; + private Map mOverrideItemScreenSizeFactors = Maps.newHashMap(); + + private int mScreenHeight; + + // whether to include a text view above the list + private boolean mIncludeHeader; + + // separators + private Set mUnselectableItems = new HashSet(); + + private boolean mStackFromBottom; + + private int mClickedPosition = -1; + + private int mLongClickedPosition = -1; + + private int mConvertMisses = 0; + + private int mHeaderViewCount; + private boolean mHeadersFocusable; + + private int mFooterViewCount; + private LinearLayout mLinearLayout; + + public ListView getListView() { + return mListView; + } + + protected int getScreenHeight() { + return mScreenHeight; + } + + /** + * Return whether the item at position is selectable (i.e is a separator). + * (external users can access this info using the adapter) + */ + private boolean isItemAtPositionSelectable(int position) { + return !mUnselectableItems.contains(position); + } + + /** + * Better way to pass in optional params than a honkin' paramater list :) + */ + public static class Params { + private int mNumItems = 4; + private boolean mItemsFocusable = false; + private int mStartingSelectionPosition = 0; + private double mItemScreenSizeFactor = 1 / 5; + private Double mFadingEdgeScreenSizeFactor = null; + + private Map mOverrideItemScreenSizeFactors = Maps.newHashMap(); + + // separators + private List mUnselectableItems = new ArrayList(8); + // whether to include a text view above the list + private boolean mIncludeHeader = false; + private boolean mStackFromBottom = false; + public boolean mMustFillScreen = true; + private int mHeaderViewCount; + private boolean mHeaderFocusable = false; + private int mFooterViewCount; + + private boolean mConnectAdapter = true; + + /** + * Set the number of items in the list. + */ + public Params setNumItems(int numItems) { + mNumItems = numItems; + return this; + } + + /** + * Set whether the items are focusable. + */ + public Params setItemsFocusable(boolean itemsFocusable) { + mItemsFocusable = itemsFocusable; + return this; + } + + /** + * Set the position that starts selected. + * + * @param startingSelectionPosition The selected position within the adapter's data set. + * Pass -1 if you do not want to force a selection. + * @return + */ + public Params setStartingSelectionPosition(int startingSelectionPosition) { + mStartingSelectionPosition = startingSelectionPosition; + return this; + } + + /** + * Set the factor that determines how tall each item is in relation to the + * screen height. + */ + public Params setItemScreenSizeFactor(double itemScreenSizeFactor) { + mItemScreenSizeFactor = itemScreenSizeFactor; + return this; + } + + /** + * Override the item screen size factor for a particular item. Useful for + * creating lists with non-uniform item height. + * @param position The position in the list. + * @param itemScreenSizeFactor The screen size factor to use for the height. + */ + public Params setPositionScreenSizeFactorOverride( + int position, double itemScreenSizeFactor) { + mOverrideItemScreenSizeFactors.put(position, itemScreenSizeFactor); + return this; + } + + /** + * Set a position as unselectable (a.k.a a separator) + * @param position + * @return + */ + public Params setPositionUnselectable(int position) { + mUnselectableItems.add(position); + return this; + } + + /** + * Set positions as unselectable (a.k.a a separator) + */ + public Params setPositionsUnselectable(int ...positions) { + for (int pos : positions) { + setPositionUnselectable(pos); + } + return this; + } + + /** + * Include a header text view above the list. + * @param includeHeader + * @return + */ + public Params includeHeaderAboveList(boolean includeHeader) { + mIncludeHeader = includeHeader; + return this; + } + + /** + * Sets the stacking direction + * @param stackFromBottom + * @return + */ + public Params setStackFromBottom(boolean stackFromBottom) { + mStackFromBottom = stackFromBottom; + return this; + } + + /** + * Sets whether the sum of the height of the list items must be at least the + * height of the list view. + */ + public Params setMustFillScreen(boolean fillScreen) { + mMustFillScreen = fillScreen; + return this; + } + + /** + * Set the factor for the fading edge length. + */ + public Params setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor) { + mFadingEdgeScreenSizeFactor = fadingEdgeScreenSizeFactor; + return this; + } + + /** + * Set the number of header views to appear within the list + */ + public Params setHeaderViewCount(int headerViewCount) { + mHeaderViewCount = headerViewCount; + return this; + } + + /** + * Set whether the headers should be focusable. + * @param headerFocusable Whether the headers should be focusable (i.e + * created as edit texts rather than text views). + */ + public Params setHeaderFocusable(boolean headerFocusable) { + mHeaderFocusable = headerFocusable; + return this; + } + + /** + * Set the number of footer views to appear within the list + */ + public Params setFooterViewCount(int footerViewCount) { + mFooterViewCount = footerViewCount; + return this; + } + + /** + * Sets whether the {@link ListScenario} will automatically set the + * adapter on the list view. If this is false, the client MUST set it + * manually (this is useful when adding headers to the list view, which + * must be done before the adapter is set). + */ + public Params setConnectAdapter(boolean connectAdapter) { + mConnectAdapter = connectAdapter; + return this; + } + } + + /** + * How each scenario customizes its behavior. + * @param params + */ + protected abstract void init(Params params); + + /** + * Override this if you want to know when something has been selected (perhaps + * more importantly, that {@link android.widget.AdapterView.OnItemSelectedListener} has + * been triggered). + */ + protected void positionSelected(int positon) { + } + + /** + * Override this if you want to know that nothing is selected. + */ + protected void nothingSelected() { + } + + /** + * Override this if you want to know when something has been clicked (perhaps + * more importantly, that {@link android.widget.AdapterView.OnItemClickListener} has + * been triggered). + */ + protected void positionClicked(int position) { + setClickedPosition(position); + } + + /** + * Override this if you want to know when something has been long clicked (perhaps + * more importantly, that {@link android.widget.AdapterView.OnItemLongClickListener} has + * been triggered). + */ + protected void positionLongClicked(int position) { + setLongClickedPosition(position); + } + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // for test stability, turn off title bar + requestWindowFeature(Window.FEATURE_NO_TITLE); + + + mScreenHeight = getWindowManager().getDefaultDisplay().getHeight(); + + final Params params = createParams(); + init(params); + + readAndValidateParams(params); + + + mListView = createListView(); + mListView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mListView.setDrawSelectorOnTop(false); + + for (int i=0; i= 0) { + mListView.setSelection(mStartingSelectionPosition); + } + mListView.setPadding(0, 0, 0, 0); + mListView.setStackFromBottom(mStackFromBottom); + mListView.setDivider(null); + + mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View v, int position, long id) { + positionSelected(position); + } + + public void onNothingSelected(AdapterView parent) { + nothingSelected(); + } + }); + + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView parent, View v, int position, long id) { + positionClicked(position); + } + }); + + // set the fading edge length porportionally to the screen + // height for test stability + if (params.mFadingEdgeScreenSizeFactor != null) { + mListView.setFadingEdgeLength((int) (params.mFadingEdgeScreenSizeFactor * mScreenHeight)); + } else { + mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight)); + } + + if (mIncludeHeader) { + mLinearLayout = new LinearLayout(this); + + mHeaderTextView = new TextView(this); + mHeaderTextView.setText("hi"); + mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + mLinearLayout.addView(mHeaderTextView); + + mLinearLayout.setOrientation(LinearLayout.VERTICAL); + mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mListView.setLayoutParams((new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + 0, + 1f))); + + mLinearLayout.addView(mListView); + setContentView(mLinearLayout); + } else { + mLinearLayout = new LinearLayout(this); + mLinearLayout.setOrientation(LinearLayout.VERTICAL); + mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mListView.setLayoutParams((new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + 0, + 1f))); + mLinearLayout.addView(mListView); + setContentView(mLinearLayout); + } + } + + /** + * Returns the LinearLayout containing the ListView in this scenario. + * + * @return The LinearLayout in which the ListView is held. + */ + protected LinearLayout getListViewContainer() { + return mLinearLayout; + } + + /** + * Attaches a long press listener. You can find out which views were clicked by calling + * {@link #getLongClickedPosition()}. + */ + public void enableLongPress() { + mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + public boolean onItemLongClick(AdapterView parent, View v, int position, long id) { + positionLongClicked(position); + return true; + } + }); + } + + /** + * @return The newly created ListView widget. + */ + protected ListView createListView() { + return new ListView(this); + } + + /** + * @return The newly created Params object. + */ + protected Params createParams() { + return new Params(); + } + + /** + * Sets an adapter on a ListView. + * + * @param listView The ListView to set the adapter on. + */ + protected void setAdapter(ListView listView) { + listView.setAdapter(new MyAdapter()); + } + + /** + * Read in and validate all of the params passed in by the scenario. + * @param params + */ + protected void readAndValidateParams(Params params) { + if (params.mMustFillScreen ) { + double totalFactor = 0.0; + for (int i = 0; i < params.mNumItems; i++) { + if (params.mOverrideItemScreenSizeFactors.containsKey(i)) { + totalFactor += params.mOverrideItemScreenSizeFactors.get(i); + } else { + totalFactor += params.mItemScreenSizeFactor; + } + } + if (totalFactor < 1.0) { + throw new IllegalArgumentException("list items must combine to be at least " + + "the height of the screen. this is not the case with " + params.mNumItems + + " items and " + params.mItemScreenSizeFactor + " screen factor and " + + "screen height of " + mScreenHeight); + } + } + + mNumItems = params.mNumItems; + mItemsFocusable = params.mItemsFocusable; + mStartingSelectionPosition = params.mStartingSelectionPosition; + mItemScreenSizeFactor = params.mItemScreenSizeFactor; + + mOverrideItemScreenSizeFactors.putAll(params.mOverrideItemScreenSizeFactors); + + mUnselectableItems.addAll(params.mUnselectableItems); + mIncludeHeader = params.mIncludeHeader; + mStackFromBottom = params.mStackFromBottom; + mHeaderViewCount = params.mHeaderViewCount; + mHeadersFocusable = params.mHeaderFocusable; + mFooterViewCount = params.mFooterViewCount; + } + + public final String getValueAtPosition(int position) { + return isItemAtPositionSelectable(position) + ? + "position " + position: + "------- " + position; + } + + /** + * @return The height that will be set for a particular position. + */ + public int getHeightForPosition(int position) { + int desiredHeight = (int) (mScreenHeight * mItemScreenSizeFactor); + if (mOverrideItemScreenSizeFactors.containsKey(position)) { + desiredHeight = (int) (mScreenHeight * mOverrideItemScreenSizeFactors.get(position)); + } + return desiredHeight; + } + + + /** + * @return The contents of the header above the list. + * @throws IllegalArgumentException if there is no header. + */ + public final String getHeaderValue() { + if (!mIncludeHeader) { + throw new IllegalArgumentException("no header above list"); + } + return mHeaderTextView.getText().toString(); + } + + /** + * @param value What to put in the header text view + * @throws IllegalArgumentException if there is no header. + */ + protected final void setHeaderValue(String value) { + if (!mIncludeHeader) { + throw new IllegalArgumentException("no header above list"); + } + mHeaderTextView.setText(value); + } + + /** + * Create a view for a list item. Override this to create a custom view beyond + * the simple focusable / unfocusable text view. + * @param position The position. + * @param parent The parent + * @param desiredHeight The height the view should be to respect the desired item + * to screen height ratio. + * @return a view for the list. + */ + protected View createView(int position, ViewGroup parent, int desiredHeight) { + return ListItemFactory.text(position, parent.getContext(), getValueAtPosition(position), + desiredHeight); + } + + /** + * Convert a non-null view. + */ + public View convertView(int position, View convertView, ViewGroup parent) { + return ListItemFactory.convertText(convertView, getValueAtPosition(position), position); + } + + public void setClickedPosition(int clickedPosition) { + mClickedPosition = clickedPosition; + } + + public int getClickedPosition() { + return mClickedPosition; + } + + public void setLongClickedPosition(int longClickedPosition) { + mLongClickedPosition = longClickedPosition; + } + + public int getLongClickedPosition() { + return mLongClickedPosition; + } + + /** + * Have a child of the list view call {@link View#requestRectangleOnScreen(android.graphics.Rect)}. + * @param childIndex The index into the viewgroup children (i.e the children that are + * currently visible). + * @param rect The rectangle, in the child's coordinates. + */ + public void requestRectangleOnScreen(int childIndex, final Rect rect) { + final View child = getListView().getChildAt(childIndex); + + child.post(new Runnable() { + public void run() { + child.requestRectangleOnScreen(rect); + } + }); + } + + /** + * Return an item type for the specified position in the adapter. Override if your + * adapter creates more than one type. + */ + public int getItemViewType(int position) { + return 0; + } + + /** + * Return the number of types created by the adapter. Override if your + * adapter creates more than one type. + */ + public int getViewTypeCount() { + return 1; + } + + /** + * @return The number of times convertView failed + */ + public int getConvertMisses() { + return mConvertMisses; + } + + private class MyAdapter extends BaseAdapter { + + public int getCount() { + return mNumItems; + } + + public Object getItem(int position) { + return getValueAtPosition(position); + } + + public long getItemId(int position) { + return position; + } + + @Override + public boolean areAllItemsEnabled() { + return mUnselectableItems.isEmpty(); + } + + @Override + public boolean isEnabled(int position) { + return isItemAtPositionSelectable(position); + } + + public View getView(int position, View convertView, ViewGroup parent) { + View result = null; + if (position >= mNumItems || position < 0) { + throw new IllegalStateException("position out of range for adapter!"); + } + + if (convertView != null) { + result = convertView(position, convertView, parent); + if (result == null) { + mConvertMisses++; + } + } + + if (result == null) { + int desiredHeight = getHeightForPosition(position); + result = createView(position, parent, desiredHeight); + } + return result; + } + + @Override + public int getItemViewType(int position) { + return ListScenario.this.getItemViewType(position); + } + + @Override + public int getViewTypeCount() { + return ListScenario.this.getViewTypeCount(); + } + + } +} diff --git a/src/main/java/android/util/ListUtil.java b/src/main/java/android/util/ListUtil.java new file mode 100644 index 0000000..2a7cb96 --- /dev/null +++ b/src/main/java/android/util/ListUtil.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2007 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 android.util; + +import android.app.Instrumentation; +import android.view.KeyEvent; +import android.widget.ListView; + + +/** + * Various useful stuff for instrumentation testing listview. + */ +public class ListUtil { + + + private final ListView mListView; + private final Instrumentation mInstrumentation; + + /** + * @param listView The listview to act on + * @param instrumentation The instrumentation to use. + */ + public ListUtil(ListView listView, Instrumentation instrumentation) { + mListView = listView; + mInstrumentation = instrumentation; + } + + /** + * Set the selected position of the list view. + * @param pos The desired position. + */ + public final void setSelectedPosition(final int pos) { + mListView.post(new Runnable() { + public void run() { + mListView.setSelection(pos); + } + }); + mInstrumentation.waitForIdleSync(); + } + + /** + * Get the top of the list. + */ + public final int getListTop() { + return mListView.getListPaddingTop(); + } + + /** + * Get the bottom of the list. + */ + public final int getListBottom() { + return mListView.getHeight() - mListView.getListPaddingBottom(); + } + + /** + * Arrow (up or down as appropriate) to the desired position in the list. + * @param desiredPos The desired position + * @throws IllegalStateException if the position can't be reached within 20 presses. + */ + public final void arrowScrollToSelectedPosition(int desiredPos) { + if (desiredPos > mListView.getSelectedItemPosition()) { + arrowDownToSelectedPosition(desiredPos); + } else { + arrowUpToSelectedPosition(desiredPos); + } + } + + private void arrowDownToSelectedPosition(int position) { + int maxDowns = 20; + while(mListView.getSelectedItemPosition() < position && --maxDowns > 0) { + mInstrumentation.sendCharacterSync(KeyEvent.KEYCODE_DPAD_DOWN); + } + if (position != mListView.getSelectedItemPosition()) { + throw new IllegalStateException("couldn't get to item after 20 downs"); + } + + } + + private void arrowUpToSelectedPosition(int position) { + int maxUps = 20; + while(mListView.getSelectedItemPosition() > position && --maxUps > 0) { + mInstrumentation.sendCharacterSync(KeyEvent.KEYCODE_DPAD_UP); + } + if (position != mListView.getSelectedItemPosition()) { + throw new IllegalStateException("couldn't get to item after 20 ups"); + } + } + +} diff --git a/src/main/java/android/util/LocalLog.java b/src/main/java/android/util/LocalLog.java new file mode 100644 index 0000000..e49b8c3 --- /dev/null +++ b/src/main/java/android/util/LocalLog.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2006 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 android.util; + +import android.text.format.Time; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Calendar; +import java.util.Iterator; +import java.util.LinkedList; + +/** + * @hide + */ +public final class LocalLog { + + private LinkedList mLog; + private int mMaxLines; + private long mNow; + + public LocalLog(int maxLines) { + mLog = new LinkedList(); + mMaxLines = maxLines; + } + + public synchronized void log(String msg) { + if (mMaxLines > 0) { + mNow = System.currentTimeMillis(); + StringBuilder sb = new StringBuilder(); + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(mNow); + sb.append(String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c)); + mLog.add(sb.toString() + " - " + msg); + while (mLog.size() > mMaxLines) mLog.remove(); + } + } + + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + Iterator itr = mLog.listIterator(0); + while (itr.hasNext()) { + pw.println(itr.next()); + } + } +} diff --git a/src/main/java/android/util/Log.java b/src/main/java/android/util/Log.java new file mode 100644 index 0000000..b94e48b --- /dev/null +++ b/src/main/java/android/util/Log.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2006 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 android.util; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.UnknownHostException; + +/** + * Mock Log implementation for testing on non android host. + */ +public final class Log { + + /** + * Priority constant for the println method; use Log.v. + */ + public static final int VERBOSE = 2; + + /** + * Priority constant for the println method; use Log.d. + */ + public static final int DEBUG = 3; + + /** + * Priority constant for the println method; use Log.i. + */ + public static final int INFO = 4; + + /** + * Priority constant for the println method; use Log.w. + */ + public static final int WARN = 5; + + /** + * Priority constant for the println method; use Log.e. + */ + public static final int ERROR = 6; + + /** + * Priority constant for the println method. + */ + public static final int ASSERT = 7; + + private Log() { + } + + /** + * Send a {@link #VERBOSE} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + public static int v(String tag, String msg) { + return println(LOG_ID_MAIN, VERBOSE, tag, msg); + } + + /** + * Send a {@link #VERBOSE} log message and log the exception. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + public static int v(String tag, String msg, Throwable tr) { + return println(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr)); + } + + /** + * Send a {@link #DEBUG} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + public static int d(String tag, String msg) { + return println(LOG_ID_MAIN, DEBUG, tag, msg); + } + + /** + * Send a {@link #DEBUG} log message and log the exception. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + public static int d(String tag, String msg, Throwable tr) { + return println(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr)); + } + + /** + * Send an {@link #INFO} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + public static int i(String tag, String msg) { + return println(LOG_ID_MAIN, INFO, tag, msg); + } + + /** + * Send a {@link #INFO} log message and log the exception. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + public static int i(String tag, String msg, Throwable tr) { + return println(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr)); + } + + /** + * Send a {@link #WARN} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + public static int w(String tag, String msg) { + return println(LOG_ID_MAIN, WARN, tag, msg); + } + + /** + * Send a {@link #WARN} log message and log the exception. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + public static int w(String tag, String msg, Throwable tr) { + return println(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr)); + } + + /* + * Send a {@link #WARN} log message and log the exception. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param tr An exception to log + */ + public static int w(String tag, Throwable tr) { + return println(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr)); + } + + /** + * Send an {@link #ERROR} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + public static int e(String tag, String msg) { + return println(LOG_ID_MAIN, ERROR, tag, msg); + } + + /** + * Send a {@link #ERROR} log message and log the exception. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + public static int e(String tag, String msg, Throwable tr) { + return println(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr)); + } + + /** + * Handy function to get a loggable stack trace from a Throwable + * @param tr An exception to log + */ + public static String getStackTraceString(Throwable tr) { + if (tr == null) { + return ""; + } + + // This is to reduce the amount of log spew that apps do in the non-error + // condition of the network being unavailable. + Throwable t = tr; + while (t != null) { + if (t instanceof UnknownHostException) { + return ""; + } + t = t.getCause(); + } + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + tr.printStackTrace(pw); + pw.flush(); + return sw.toString(); + } + + /** + * Low-level logging call. + * @param priority The priority/type of this log message + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @return The number of bytes written. + */ + public static int println(int priority, String tag, String msg) { + return println(LOG_ID_MAIN, priority, tag, msg); + } + + /** @hide */ public static final int LOG_ID_MAIN = 0; + /** @hide */ public static final int LOG_ID_RADIO = 1; + /** @hide */ public static final int LOG_ID_EVENTS = 2; + /** @hide */ public static final int LOG_ID_SYSTEM = 3; + /** @hide */ public static final int LOG_ID_CRASH = 4; + + /** @hide */ @SuppressWarnings("unused") + public static int println(int bufID, + int priority, String tag, String msg) { + return 0; + } +} diff --git a/src/main/java/android/util/LogPrinter.java b/src/main/java/android/util/LogPrinter.java new file mode 100644 index 0000000..68f64d0 --- /dev/null +++ b/src/main/java/android/util/LogPrinter.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2006 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 android.util; + +/** + * Implementation of a {@link android.util.Printer} that sends its output + * to the system log. + */ +public class LogPrinter implements Printer { + private final int mPriority; + private final String mTag; + private final int mBuffer; + + /** + * Create a new Printer that sends to the log with the given priority + * and tag. + * + * @param priority The desired log priority: + * {@link android.util.Log#VERBOSE Log.VERBOSE}, + * {@link android.util.Log#DEBUG Log.DEBUG}, + * {@link android.util.Log#INFO Log.INFO}, + * {@link android.util.Log#WARN Log.WARN}, or + * {@link android.util.Log#ERROR Log.ERROR}. + * @param tag A string tag to associate with each printed log statement. + */ + public LogPrinter(int priority, String tag) { + mPriority = priority; + mTag = tag; + mBuffer = Log.LOG_ID_MAIN; + } + + /** + * @hide + * Same as above, but buffer is one of the LOG_ID_ constants from android.util.Log. + */ + public LogPrinter(int priority, String tag, int buffer) { + mPriority = priority; + mTag = tag; + mBuffer = buffer; + } + + public void println(String x) { + Log.println_native(mBuffer, mPriority, mTag, x); + } +} diff --git a/src/main/java/android/util/LogTest.java b/src/main/java/android/util/LogTest.java new file mode 100644 index 0000000..30c81b0 --- /dev/null +++ b/src/main/java/android/util/LogTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2007 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 android.util; + +import junit.framework.Assert; +import junit.framework.TestCase; + +import android.os.SystemProperties; +import android.test.PerformanceTestCase; +import android.test.suitebuilder.annotation.Suppress; +import android.util.Log; + +//This is an empty TestCase. +@Suppress +public class LogTest extends TestCase { + private static final String PROPERTY_TAG = "log.tag.LogTest"; + private static final String LOG_TAG = "LogTest"; + + + // TODO: remove this test once we uncomment out the following test. + public void testLogTestDummy() { + return; + } + + + /* TODO: This test is commented out because we will not be able to set properities. Fix the test. + public void testIsLoggable() { + // First clear any SystemProperty setting for our test key. + SystemProperties.set(PROPERTY_TAG, null); + + String value = SystemProperties.get(PROPERTY_TAG); + Assert.assertTrue(value == null || value.length() == 0); + + // Check to make sure that all levels expect for INFO, WARN, ERROR, and ASSERT are loggable. + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.VERBOSE)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.DEBUG)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.INFO)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.WARN)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ERROR)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ASSERT)); + + // Set the log level to be VERBOSE for this tag. + SystemProperties.set(PROPERTY_TAG, "VERBOSE"); + + // Test to make sure all log levels >= VERBOSE are loggable. + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.VERBOSE)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.DEBUG)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.INFO)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.WARN)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ERROR)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ASSERT)); + + // Set the log level to be DEBUG for this tag. + SystemProperties.set(PROPERTY_TAG, "DEBUG"); + + // Test to make sure all log levels >= DEBUG are loggable. + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.VERBOSE)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.DEBUG)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.INFO)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.WARN)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ERROR)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ASSERT)); + + // Set the log level to be INFO for this tag. + SystemProperties.set(PROPERTY_TAG, "INFO"); + + // Test to make sure all log levels >= INFO are loggable. + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.VERBOSE)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.DEBUG)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.INFO)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.WARN)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ERROR)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ASSERT)); + + // Set the log level to be WARN for this tag. + SystemProperties.set(PROPERTY_TAG, "WARN"); + + // Test to make sure all log levels >= WARN are loggable. + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.VERBOSE)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.DEBUG)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.INFO)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.WARN)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ERROR)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ASSERT)); + + // Set the log level to be ERROR for this tag. + SystemProperties.set(PROPERTY_TAG, "ERROR"); + + // Test to make sure all log levels >= ERROR are loggable. + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.VERBOSE)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.DEBUG)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.INFO)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.WARN)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ERROR)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ASSERT)); + + // Set the log level to be ASSERT for this tag. + SystemProperties.set(PROPERTY_TAG, "ASSERT"); + + // Test to make sure all log levels >= ASSERT are loggable. + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.VERBOSE)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.DEBUG)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.INFO)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.WARN)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.ERROR)); + Assert.assertTrue(Log.isLoggable(LOG_TAG, Log.ASSERT)); + + // Set the log level to be SUPPRESS for this tag. + SystemProperties.set(PROPERTY_TAG, "SUPPRESS"); + + // Test to make sure all log levels >= ASSERT are loggable. + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.VERBOSE)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.DEBUG)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.INFO)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.WARN)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.ERROR)); + Assert.assertFalse(Log.isLoggable(LOG_TAG, Log.ASSERT)); + } + */ + + public static class PerformanceTest extends TestCase implements PerformanceTestCase { + private static final int ITERATIONS = 1000; + + @Override + public void setUp() { + SystemProperties.set(LOG_TAG, "VERBOSE"); + } + + public boolean isPerformanceOnly() { + return true; + } + + public int startPerformance(PerformanceTestCase.Intermediates intermediates) { + intermediates.setInternalIterations(ITERATIONS * 10); + return 0; + } + + public void testIsLoggable() { + boolean canLog = false; + for (int i = ITERATIONS - 1; i >= 0; i--) { + canLog = Log.isLoggable(LOG_TAG, Log.VERBOSE); + canLog = Log.isLoggable(LOG_TAG, Log.VERBOSE); + canLog = Log.isLoggable(LOG_TAG, Log.VERBOSE); + canLog = Log.isLoggable(LOG_TAG, Log.VERBOSE); + canLog = Log.isLoggable(LOG_TAG, Log.VERBOSE); + canLog = Log.isLoggable(LOG_TAG, Log.VERBOSE); + canLog = Log.isLoggable(LOG_TAG, Log.VERBOSE); + canLog = Log.isLoggable(LOG_TAG, Log.VERBOSE); + canLog = Log.isLoggable(LOG_TAG, Log.VERBOSE); + canLog = Log.isLoggable(LOG_TAG, Log.VERBOSE); + } + } + } +} diff --git a/src/main/java/android/util/LogWriter.java b/src/main/java/android/util/LogWriter.java new file mode 100644 index 0000000..ce30631 --- /dev/null +++ b/src/main/java/android/util/LogWriter.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2011 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 android.util; + +import java.io.Writer; + +/** @hide */ +public class LogWriter extends Writer { + private final int mPriority; + private final String mTag; + private final int mBuffer; + private StringBuilder mBuilder = new StringBuilder(128); + + /** + * Create a new Writer that sends to the log with the given priority + * and tag. + * + * @param priority The desired log priority: + * {@link android.util.Log#VERBOSE Log.VERBOSE}, + * {@link android.util.Log#DEBUG Log.DEBUG}, + * {@link android.util.Log#INFO Log.INFO}, + * {@link android.util.Log#WARN Log.WARN}, or + * {@link android.util.Log#ERROR Log.ERROR}. + * @param tag A string tag to associate with each printed log statement. + */ + public LogWriter(int priority, String tag) { + mPriority = priority; + mTag = tag; + mBuffer = Log.LOG_ID_MAIN; + } + + /** + * @hide + * Same as above, but buffer is one of the LOG_ID_ constants from android.util.Log. + */ + public LogWriter(int priority, String tag, int buffer) { + mPriority = priority; + mTag = tag; + mBuffer = buffer; + } + + @Override public void close() { + flushBuilder(); + } + + @Override public void flush() { + flushBuilder(); + } + + @Override public void write(char[] buf, int offset, int count) { + for(int i = 0; i < count; i++) { + char c = buf[offset + i]; + if ( c == '\n') { + flushBuilder(); + } + else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.println_native(mBuffer, mPriority, mTag, mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } +} diff --git a/src/main/java/android/util/Log_Delegate.java b/src/main/java/android/util/Log_Delegate.java new file mode 100644 index 0000000..7f432ab --- /dev/null +++ b/src/main/java/android/util/Log_Delegate.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2011 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 android.util; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +class Log_Delegate { + // to replicate prefix visible when using 'adb logcat' + private static char priorityChar(int priority) { + switch (priority) { + case Log.VERBOSE: + return 'V'; + case Log.DEBUG: + return 'D'; + case Log.INFO: + return 'I'; + case Log.WARN: + return 'W'; + case Log.ERROR: + return 'E'; + case Log.ASSERT: + return 'A'; + default: + return '?'; + } + } + + @LayoutlibDelegate + static int println_native(int bufID, int priority, String tag, String msgs) { + String prefix = priorityChar(priority) + "/" + tag + ": "; + for (String msg: msgs.split("\n")) { + System.out.println(prefix + msg); + } + return 0; + } + +} diff --git a/src/main/java/android/util/LongArray.java b/src/main/java/android/util/LongArray.java new file mode 100644 index 0000000..54a6882 --- /dev/null +++ b/src/main/java/android/util/LongArray.java @@ -0,0 +1,166 @@ +/* + * 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 android.util; + +import com.android.internal.util.ArrayUtils; +import libcore.util.EmptyArray; + +/** + * Implements a growing array of long primitives. + * + * @hide + */ +public class LongArray implements Cloneable { + private static final int MIN_CAPACITY_INCREMENT = 12; + + private long[] mValues; + private int mSize; + + /** + * Creates an empty LongArray with the default initial capacity. + */ + public LongArray() { + this(10); + } + + /** + * Creates an empty LongArray with the specified initial capacity. + */ + public LongArray(int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.LONG; + } else { + mValues = ArrayUtils.newUnpaddedLongArray(initialCapacity); + } + mSize = 0; + } + + /** + * Appends the specified value to the end of this array. + */ + public void add(long value) { + add(mSize, value); + } + + /** + * Inserts a value at the specified position in this array. + * + * @throws IndexOutOfBoundsException when index < 0 || index > size() + */ + public void add(int index, long value) { + if (index < 0 || index > mSize) { + throw new IndexOutOfBoundsException(); + } + + ensureCapacity(1); + + if (mSize - index != 0) { + System.arraycopy(mValues, index, mValues, index + 1, mSize - index); + } + + mValues[index] = value; + mSize++; + } + + /** + * Adds the values in the specified array to this array. + */ + public void addAll(LongArray values) { + final int count = values.mSize; + ensureCapacity(count); + + System.arraycopy(values.mValues, 0, mValues, mSize, count); + mSize += count; + } + + /** + * Ensures capacity to append at least count values. + */ + private void ensureCapacity(int count) { + final int currentSize = mSize; + final int minCapacity = currentSize + count; + if (minCapacity >= mValues.length) { + final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ? + MIN_CAPACITY_INCREMENT : currentSize >> 1); + final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity; + final long[] newValues = ArrayUtils.newUnpaddedLongArray(newCapacity); + System.arraycopy(mValues, 0, newValues, 0, currentSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + @Override + public LongArray clone() { + LongArray clone = null; + try { + clone = (LongArray) super.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Returns the value at the specified position in this array. + */ + public long get(int index) { + if (index >= mSize) { + throw new ArrayIndexOutOfBoundsException(mSize, index); + } + return mValues[index]; + } + + /** + * Returns the index of the first occurrence of the specified value in this + * array, or -1 if this array does not contain the value. + */ + public int indexOf(long value) { + final int n = mSize; + for (int i = 0; i < n; i++) { + if (mValues[i] == value) { + return i; + } + } + return -1; + } + + /** + * Removes the value at the specified index from this array. + */ + public void remove(int index) { + if (index >= mSize) { + throw new ArrayIndexOutOfBoundsException(mSize, index); + } + System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1); + mSize--; + } + + /** + * Returns the number of values in this array. + */ + public int size() { + return mSize; + } +} diff --git a/src/main/java/android/util/LongSparseArray.java b/src/main/java/android/util/LongSparseArray.java new file mode 100644 index 0000000..6b45ff4 --- /dev/null +++ b/src/main/java/android/util/LongSparseArray.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2009 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 android.util; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; + +/** + * SparseArray mapping longs to Objects. Unlike a normal array of Objects, + * there can be gaps in the indices. It is intended to be more memory efficient + * than using a HashMap to map Longs to Objects, both because it avoids + * auto-boxing keys and its data structure doesn't rely on an extra entry object + * for each mapping. + * + *

    Note that this container keeps its mappings in an array data structure, + * using a binary search to find keys. The implementation is not intended to be appropriate for + * data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    To help with performance, the container includes an optimization when removing + * keys: instead of compacting its array immediately, it leaves the removed entry marked + * as deleted. The entry can then be re-used for the same key, or compacted later in + * a single garbage collection step of all removed entries. This garbage collection will + * need to be performed at any time the array needs to be grown or the the map size or + * entry values are retrieved.

    + * + *

    It is possible to iterate over the items in this container using + * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using + * keyAt(int) with ascending values of the index will return the + * keys in ascending order, or the values corresponding to the keys in ascending + * order in the case of valueAt(int).

    + */ +public class LongSparseArray implements Cloneable { + private static final Object DELETED = new Object(); + private boolean mGarbage = false; + + private long[] mKeys; + private Object[] mValues; + private int mSize; + + /** + * Creates a new LongSparseArray containing no mappings. + */ + public LongSparseArray() { + this(10); + } + + /** + * Creates a new LongSparseArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public LongSparseArray(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = EmptyArray.LONG; + mValues = EmptyArray.OBJECT; + } else { + mKeys = ArrayUtils.newUnpaddedLongArray(initialCapacity); + mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity); + } + mSize = 0; + } + + @Override + @SuppressWarnings("unchecked") + public LongSparseArray clone() { + LongSparseArray clone = null; + try { + clone = (LongSparseArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the Object mapped from the specified key, or null + * if no such mapping has been made. + */ + public E get(long key) { + return get(key, null); + } + + /** + * Gets the Object mapped from the specified key, or the specified Object + * if no such mapping has been made. + */ + @SuppressWarnings("unchecked") + public E get(long key, E valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i < 0 || mValues[i] == DELETED) { + return valueIfKeyNotFound; + } else { + return (E) mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(long key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + if (mValues[i] != DELETED) { + mValues[i] = DELETED; + mGarbage = true; + } + } + } + + /** + * Alias for {@link #delete(long)}. + */ + public void remove(long key) { + delete(key); + } + + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + if (mValues[index] != DELETED) { + mValues[index] = DELETED; + mGarbage = true; + } + } + + private void gc() { + // Log.e("SparseArray", "gc start with " + mSize); + + int n = mSize; + int o = 0; + long[] keys = mKeys; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + Object val = values[i]; + + if (val != DELETED) { + if (i != o) { + keys[o] = keys[i]; + values[o] = val; + values[i] = null; + } + + o++; + } + } + + mGarbage = false; + mSize = o; + + // Log.e("SparseArray", "gc end with " + mSize); + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(long key, E value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + if (i < mSize && mValues[i] == DELETED) { + mKeys[i] = key; + mValues[i] = value; + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + + // Search again because indices may have changed. + i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this LongSparseArray + * currently stores. + */ + public int size() { + if (mGarbage) { + gc(); + } + + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * LongSparseArray stores. + * + *

    The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., keyAt(0) will return the + * smallest key and keyAt(size()-1) will return the largest + * key.

    + */ + public long keyAt(int index) { + if (mGarbage) { + gc(); + } + + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * LongSparseArray stores. + * + *

    The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * valueAt(0) will return the value associated with the + * smallest key and valueAt(size()-1) will return the value + * associated with the largest key.

    + */ + @SuppressWarnings("unchecked") + public E valueAt(int index) { + if (mGarbage) { + gc(); + } + + return (E) mValues[index]; + } + + /** + * Given an index in the range 0...size()-1, sets a new + * value for the indexth key-value mapping that this + * LongSparseArray stores. + */ + public void setValueAt(int index, E value) { + if (mGarbage) { + gc(); + } + + mValues[index] = value; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(long key) { + if (mGarbage) { + gc(); + } + + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(E value) { + if (mGarbage) { + gc(); + } + + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this LongSparseArray. + */ + public void clear() { + int n = mSize; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + values[i] = null; + } + + mSize = 0; + mGarbage = false; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(long key, E value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + } + + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; + } + + /** + * {@inheritDoc} + * + *

    This implementation composes a string by iterating over its mappings. If + * this map contains itself as a value, the string "(this Map)" + * will appear in its place. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + long key = keyAt(i); + buffer.append(key); + buffer.append('='); + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Map)"); + } + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/src/main/java/android/util/LongSparseLongArray.java b/src/main/java/android/util/LongSparseLongArray.java new file mode 100644 index 0000000..a361457 --- /dev/null +++ b/src/main/java/android/util/LongSparseLongArray.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2007 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 android.util; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; + +/** + * Map of {@code long} to {@code long}. Unlike a normal array of longs, there + * can be gaps in the indices. It is intended to be more memory efficient than using a + * {@code HashMap}, both because it avoids + * auto-boxing keys and values and its data structure doesn't rely on an extra entry object + * for each mapping. + * + *

    Note that this container keeps its mappings in an array data structure, + * using a binary search to find keys. The implementation is not intended to be appropriate for + * data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    It is possible to iterate over the items in this container using + * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using + * keyAt(int) with ascending values of the index will return the + * keys in ascending order, or the values corresponding to the keys in ascending + * order in the case of valueAt(int).

    + * + * @hide + */ +public class LongSparseLongArray implements Cloneable { + private long[] mKeys; + private long[] mValues; + private int mSize; + + /** + * Creates a new SparseLongArray containing no mappings. + */ + public LongSparseLongArray() { + this(10); + } + + /** + * Creates a new SparseLongArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public LongSparseLongArray(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = EmptyArray.LONG; + mValues = EmptyArray.LONG; + } else { + mKeys = ArrayUtils.newUnpaddedLongArray(initialCapacity); + mValues = new long[mKeys.length]; + } + mSize = 0; + } + + @Override + public LongSparseLongArray clone() { + LongSparseLongArray clone = null; + try { + clone = (LongSparseLongArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the long mapped from the specified key, or 0 + * if no such mapping has been made. + */ + public long get(long key) { + return get(key, 0); + } + + /** + * Gets the long mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public long get(long key, long valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i < 0) { + return valueIfKeyNotFound; + } else { + return mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(long key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + removeAt(i); + } + } + + /** + * Removes the mapping at the given index. + */ + public void removeAt(int index) { + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1)); + mSize--; + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(long key, long value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseIntArray + * currently stores. + */ + public int size() { + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * SparseLongArray stores. + * + *

    The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., keyAt(0) will return the + * smallest key and keyAt(size()-1) will return the largest + * key.

    + */ + public long keyAt(int index) { + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * SparseLongArray stores. + * + *

    The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * valueAt(0) will return the value associated with the + * smallest key and valueAt(size()-1) will return the value + * associated with the largest key.

    + */ + public long valueAt(int index) { + return mValues[index]; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(long key) { + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(long value) { + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseIntArray. + */ + public void clear() { + mSize = 0; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(long key, long value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; + } + + /** + * {@inheritDoc} + * + *

    This implementation composes a string by iterating over its mappings. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + long key = keyAt(i); + buffer.append(key); + buffer.append('='); + long value = valueAt(i); + buffer.append(value); + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/src/main/java/android/util/LongSparseLongArrayTest.java b/src/main/java/android/util/LongSparseLongArrayTest.java new file mode 100644 index 0000000..cb468bc --- /dev/null +++ b/src/main/java/android/util/LongSparseLongArrayTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2007 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 android.util; + +import junit.framework.TestCase; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Random; + +/** + * Tests for {@link LongSparseLongArray}. + */ +public class LongSparseLongArrayTest extends TestCase { + private static final String TAG = "LongSparseLongArrayTest"; + + public void testSimplePut() throws Exception { + final LongSparseLongArray array = new LongSparseLongArray(5); + for (int i = 0; i < 48; i++) { + final long value = 1 << i; + array.put(value, value); + } + for (int i = 0; i < 48; i++) { + final long value = 1 << i; + assertEquals(value, array.get(value, -1)); + assertEquals(-1, array.get(-value, -1)); + } + } + + public void testSimplePutBackwards() throws Exception { + final LongSparseLongArray array = new LongSparseLongArray(5); + for (int i = 47; i >= 0; i--) { + final long value = 1 << i; + array.put(value, value); + } + for (int i = 0; i < 48; i++) { + final long value = 1 << i; + assertEquals(value, array.get(value, -1)); + assertEquals(-1, array.get(-value, -1)); + } + } + + public void testMiddleInsert() throws Exception { + final LongSparseLongArray array = new LongSparseLongArray(5); + for (int i = 0; i < 48; i++) { + final long value = 1 << i; + array.put(value, value); + } + final long special = (1 << 24) + 5; + array.put(special, 1024); + for (int i = 0; i < 48; i++) { + final long value = 1 << i; + assertEquals(value, array.get(value, -1)); + assertEquals(-1, array.get(-value, -1)); + } + assertEquals(1024, array.get(special, -1)); + } + + public void testFuzz() throws Exception { + final Random r = new Random(); + + final HashMap map = new HashMap(); + final LongSparseLongArray array = new LongSparseLongArray(r.nextInt(128)); + + for (int i = 0; i < 10240; i++) { + if (r.nextBoolean()) { + final long key = r.nextLong(); + final long value = r.nextLong(); + map.put(key, value); + array.put(key, value); + } + if (r.nextBoolean() && map.size() > 0) { + final int index = r.nextInt(map.size()); + final long key = getKeyAtIndex(map, index); + map.remove(key); + array.delete(key); + } + } + + Log.d(TAG, "verifying a map with " + map.size() + " entries"); + + for (Map.Entry e : map.entrySet()) { + final long key = e.getKey(); + final long value = e.getValue(); + assertEquals(value, array.get(key)); + } + } + + private static E getKeyAtIndex(Map map, int index) { + final Iterator keys = map.keySet().iterator(); + for (int i = 0; i < index; i++) { + keys.next(); + } + return keys.next(); + } +} diff --git a/src/main/java/android/util/LruCache.java b/src/main/java/android/util/LruCache.java new file mode 100644 index 0000000..5208606 --- /dev/null +++ b/src/main/java/android/util/LruCache.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2011 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 android.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * BEGIN LAYOUTLIB CHANGE + * This is a custom version that doesn't use the non standard LinkedHashMap#eldest. + * END LAYOUTLIB CHANGE + * + * A cache that holds strong references to a limited number of values. Each time + * a value is accessed, it is moved to the head of a queue. When a value is + * added to a full cache, the value at the end of that queue is evicted and may + * become eligible for garbage collection. + * + *

    If your cached values hold resources that need to be explicitly released, + * override {@link #entryRemoved}. + * + *

    If a cache miss should be computed on demand for the corresponding keys, + * override {@link #create}. This simplifies the calling code, allowing it to + * assume a value will always be returned, even when there's a cache miss. + * + *

    By default, the cache size is measured in the number of entries. Override + * {@link #sizeOf} to size the cache in different units. For example, this cache + * is limited to 4MiB of bitmaps: + *

       {@code
    + *   int cacheSize = 4 * 1024 * 1024; // 4MiB
    + *   LruCache bitmapCache = new LruCache(cacheSize) {
    + *       protected int sizeOf(String key, Bitmap value) {
    + *           return value.getByteCount();
    + *       }
    + *   }}
    + * + *

    This class is thread-safe. Perform multiple cache operations atomically by + * synchronizing on the cache:

       {@code
    + *   synchronized (cache) {
    + *     if (cache.get(key) == null) {
    + *         cache.put(key, value);
    + *     }
    + *   }}
    + * + *

    This class does not allow null to be used as a key or value. A return + * value of null from {@link #get}, {@link #put} or {@link #remove} is + * unambiguous: the key was not in the cache. + * + *

    This class appeared in Android 3.1 (Honeycomb MR1); it's available as part + * of Android's + * Support Package for earlier releases. + */ +public class LruCache { + private final LinkedHashMap map; + + /** Size of this cache in units. Not necessarily the number of elements. */ + private int size; + private int maxSize; + + private int putCount; + private int createCount; + private int evictionCount; + private int hitCount; + private int missCount; + + /** + * @param maxSize for caches that do not override {@link #sizeOf}, this is + * the maximum number of entries in the cache. For all other caches, + * this is the maximum sum of the sizes of the entries in this cache. + */ + public LruCache(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + this.map = new LinkedHashMap(0, 0.75f, true); + } + + /** + * Sets the size of the cache. + * @param maxSize The new maximum size. + * + * @hide + */ + public void resize(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + + synchronized (this) { + this.maxSize = maxSize; + } + trimToSize(maxSize); + } + + /** + * Returns the value for {@code key} if it exists in the cache or can be + * created by {@code #create}. If a value was returned, it is moved to the + * head of the queue. This returns null if a value is not cached and cannot + * be created. + */ + public final V get(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V mapValue; + synchronized (this) { + mapValue = map.get(key); + if (mapValue != null) { + hitCount++; + return mapValue; + } + missCount++; + } + + /* + * Attempt to create a value. This may take a long time, and the map + * may be different when create() returns. If a conflicting value was + * added to the map while create() was working, we leave that value in + * the map and release the created value. + */ + + V createdValue = create(key); + if (createdValue == null) { + return null; + } + + synchronized (this) { + createCount++; + mapValue = map.put(key, createdValue); + + if (mapValue != null) { + // There was a conflict so undo that last put + map.put(key, mapValue); + } else { + size += safeSizeOf(key, createdValue); + } + } + + if (mapValue != null) { + entryRemoved(false, key, createdValue, mapValue); + return mapValue; + } else { + trimToSize(maxSize); + return createdValue; + } + } + + /** + * Caches {@code value} for {@code key}. The value is moved to the head of + * the queue. + * + * @return the previous value mapped by {@code key}. + */ + public final V put(K key, V value) { + if (key == null || value == null) { + throw new NullPointerException("key == null || value == null"); + } + + V previous; + synchronized (this) { + putCount++; + size += safeSizeOf(key, value); + previous = map.put(key, value); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, value); + } + + trimToSize(maxSize); + return previous; + } + + /** + * @param maxSize the maximum size of the cache before returning. May be -1 + * to evict even 0-sized elements. + */ + private void trimToSize(int maxSize) { + while (true) { + K key; + V value; + synchronized (this) { + if (size < 0 || (map.isEmpty() && size != 0)) { + throw new IllegalStateException(getClass().getName() + + ".sizeOf() is reporting inconsistent results!"); + } + + if (size <= maxSize) { + break; + } + + // BEGIN LAYOUTLIB CHANGE + // get the last item in the linked list. + // This is not efficient, the goal here is to minimize the changes + // compared to the platform version. + Map.Entry toEvict = null; + for (Map.Entry entry : map.entrySet()) { + toEvict = entry; + } + // END LAYOUTLIB CHANGE + + if (toEvict == null) { + break; + } + + key = toEvict.getKey(); + value = toEvict.getValue(); + map.remove(key); + size -= safeSizeOf(key, value); + evictionCount++; + } + + entryRemoved(true, key, value, null); + } + } + + /** + * Removes the entry for {@code key} if it exists. + * + * @return the previous value mapped by {@code key}. + */ + public final V remove(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V previous; + synchronized (this) { + previous = map.remove(key); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, null); + } + + return previous; + } + + /** + * Called for entries that have been evicted or removed. This method is + * invoked when a value is evicted to make space, removed by a call to + * {@link #remove}, or replaced by a call to {@link #put}. The default + * implementation does nothing. + * + *

    The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + * @param evicted true if the entry is being removed to make space, false + * if the removal was caused by a {@link #put} or {@link #remove}. + * @param newValue the new value for {@code key}, if it exists. If non-null, + * this removal was caused by a {@link #put}. Otherwise it was caused by + * an eviction or a {@link #remove}. + */ + protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} + + /** + * Called after a cache miss to compute a value for the corresponding key. + * Returns the computed value or null if no value can be computed. The + * default implementation returns null. + * + *

    The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + *

    If a value for {@code key} exists in the cache when this method + * returns, the created value will be released with {@link #entryRemoved} + * and discarded. This can occur when multiple threads request the same key + * at the same time (causing multiple values to be created), or when one + * thread calls {@link #put} while another is creating a value for the same + * key. + */ + protected V create(K key) { + return null; + } + + private int safeSizeOf(K key, V value) { + int result = sizeOf(key, value); + if (result < 0) { + throw new IllegalStateException("Negative size: " + key + "=" + value); + } + return result; + } + + /** + * Returns the size of the entry for {@code key} and {@code value} in + * user-defined units. The default implementation returns 1 so that size + * is the number of entries and max size is the maximum number of entries. + * + *

    An entry's size must not change while it is in the cache. + */ + protected int sizeOf(K key, V value) { + return 1; + } + + /** + * Clear the cache, calling {@link #entryRemoved} on each removed entry. + */ + public final void evictAll() { + trimToSize(-1); // -1 will evict 0-sized elements + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the number + * of entries in the cache. For all other caches, this returns the sum of + * the sizes of the entries in this cache. + */ + public synchronized final int size() { + return size; + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the maximum + * number of entries in the cache. For all other caches, this returns the + * maximum sum of the sizes of the entries in this cache. + */ + public synchronized final int maxSize() { + return maxSize; + } + + /** + * Returns the number of times {@link #get} returned a value that was + * already present in the cache. + */ + public synchronized final int hitCount() { + return hitCount; + } + + /** + * Returns the number of times {@link #get} returned null or required a new + * value to be created. + */ + public synchronized final int missCount() { + return missCount; + } + + /** + * Returns the number of times {@link #create(Object)} returned a value. + */ + public synchronized final int createCount() { + return createCount; + } + + /** + * Returns the number of times {@link #put} was called. + */ + public synchronized final int putCount() { + return putCount; + } + + /** + * Returns the number of values that have been evicted. + */ + public synchronized final int evictionCount() { + return evictionCount; + } + + /** + * Returns a copy of the current contents of the cache, ordered from least + * recently accessed to most recently accessed. + */ + public synchronized final Map snapshot() { + return new LinkedHashMap(map); + } + + @Override public synchronized final String toString() { + int accesses = hitCount + missCount; + int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; + return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", + maxSize, hitCount, missCount, hitPercent); + } +} diff --git a/src/main/java/android/util/LruCacheTest.java b/src/main/java/android/util/LruCacheTest.java new file mode 100644 index 0000000..5a97158 --- /dev/null +++ b/src/main/java/android/util/LruCacheTest.java @@ -0,0 +1,504 @@ +/* + * Copyright (C) 2011 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 android.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import junit.framework.TestCase; + +public final class LruCacheTest extends TestCase { + private int expectedCreateCount; + private int expectedPutCount; + private int expectedHitCount; + private int expectedMissCount; + private int expectedEvictionCount; + + public void testStatistics() { + LruCache cache = new LruCache(3); + assertStatistics(cache); + + assertEquals(null, cache.put("a", "A")); + expectedPutCount++; + assertStatistics(cache); + assertHit(cache, "a", "A"); + assertSnapshot(cache, "a", "A"); + + assertEquals(null, cache.put("b", "B")); + expectedPutCount++; + assertStatistics(cache); + assertHit(cache, "a", "A"); + assertHit(cache, "b", "B"); + assertSnapshot(cache, "a", "A", "b", "B"); + + assertEquals(null, cache.put("c", "C")); + expectedPutCount++; + assertStatistics(cache); + assertHit(cache, "a", "A"); + assertHit(cache, "b", "B"); + assertHit(cache, "c", "C"); + assertSnapshot(cache, "a", "A", "b", "B", "c", "C"); + + assertEquals(null, cache.put("d", "D")); + expectedPutCount++; + expectedEvictionCount++; // a should have been evicted + assertStatistics(cache); + assertMiss(cache, "a"); + assertHit(cache, "b", "B"); + assertHit(cache, "c", "C"); + assertHit(cache, "d", "D"); + assertHit(cache, "b", "B"); + assertHit(cache, "c", "C"); + assertSnapshot(cache, "d", "D", "b", "B", "c", "C"); + + assertEquals(null, cache.put("e", "E")); + expectedPutCount++; + expectedEvictionCount++; // d should have been evicted + assertStatistics(cache); + assertMiss(cache, "d"); + assertMiss(cache, "a"); + assertHit(cache, "e", "E"); + assertHit(cache, "b", "B"); + assertHit(cache, "c", "C"); + assertSnapshot(cache, "e", "E", "b", "B", "c", "C"); + } + + public void testStatisticsWithCreate() { + LruCache cache = newCreatingCache(); + assertStatistics(cache); + + assertCreated(cache, "aa", "created-aa"); + assertHit(cache, "aa", "created-aa"); + assertSnapshot(cache, "aa", "created-aa"); + + assertCreated(cache, "bb", "created-bb"); + assertMiss(cache, "c"); + assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb"); + + assertCreated(cache, "cc", "created-cc"); + assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb", "cc", "created-cc"); + + expectedEvictionCount++; // aa will be evicted + assertCreated(cache, "dd", "created-dd"); + assertSnapshot(cache, "bb", "created-bb", "cc", "created-cc", "dd", "created-dd"); + + expectedEvictionCount++; // bb will be evicted + assertCreated(cache, "aa", "created-aa"); + assertSnapshot(cache, "cc", "created-cc", "dd", "created-dd", "aa", "created-aa"); + } + + public void testCreateOnCacheMiss() { + LruCache cache = newCreatingCache(); + String created = cache.get("aa"); + assertEquals("created-aa", created); + } + + public void testNoCreateOnCacheHit() { + LruCache cache = newCreatingCache(); + cache.put("aa", "put-aa"); + assertEquals("put-aa", cache.get("aa")); + } + + public void testConstructorDoesNotAllowZeroCacheSize() { + try { + new LruCache(0); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testCannotPutNullKey() { + LruCache cache = new LruCache(3); + try { + cache.put(null, "A"); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testCannotPutNullValue() { + LruCache cache = new LruCache(3); + try { + cache.put("a", null); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testToString() { + LruCache cache = new LruCache(3); + assertEquals("LruCache[maxSize=3,hits=0,misses=0,hitRate=0%]", cache.toString()); + + cache.put("a", "A"); + cache.put("b", "B"); + cache.put("c", "C"); + cache.put("d", "D"); + + cache.get("a"); // miss + cache.get("b"); // hit + cache.get("c"); // hit + cache.get("d"); // hit + cache.get("e"); // miss + + assertEquals("LruCache[maxSize=3,hits=3,misses=2,hitRate=60%]", cache.toString()); + } + + public void testEvictionWithSingletonCache() { + LruCache cache = new LruCache(1); + cache.put("a", "A"); + cache.put("b", "B"); + assertSnapshot(cache, "b", "B"); + } + + public void testEntryEvictedWhenFull() { + List log = new ArrayList(); + LruCache cache = newRemovalLogCache(log); + + cache.put("a", "A"); + cache.put("b", "B"); + cache.put("c", "C"); + assertEquals(Collections.emptyList(), log); + + cache.put("d", "D"); + assertEquals(Arrays.asList("a=A"), log); + } + + /** + * Replacing the value for a key doesn't cause an eviction but it does bring + * the replaced entry to the front of the queue. + */ + public void testPutCauseEviction() { + List log = new ArrayList(); + LruCache cache = newRemovalLogCache(log); + + cache.put("a", "A"); + cache.put("b", "B"); + cache.put("c", "C"); + cache.put("b", "B2"); + assertEquals(Arrays.asList("b=B>B2"), log); + assertSnapshot(cache, "a", "A", "c", "C", "b", "B2"); + } + + public void testCustomSizesImpactsSize() { + LruCache cache = new LruCache(10) { + @Override protected int sizeOf(String key, String value) { + return key.length() + value.length(); + } + }; + + assertEquals(0, cache.size()); + cache.put("a", "AA"); + assertEquals(3, cache.size()); + cache.put("b", "BBBB"); + assertEquals(8, cache.size()); + cache.put("a", ""); + assertEquals(6, cache.size()); + } + + public void testEvictionWithCustomSizes() { + LruCache cache = new LruCache(4) { + @Override protected int sizeOf(String key, String value) { + return value.length(); + } + }; + + cache.put("a", "AAAA"); + assertSnapshot(cache, "a", "AAAA"); + cache.put("b", "BBBB"); // should evict a + assertSnapshot(cache, "b", "BBBB"); + cache.put("c", "CC"); // should evict b + assertSnapshot(cache, "c", "CC"); + cache.put("d", "DD"); + assertSnapshot(cache, "c", "CC", "d", "DD"); + cache.put("e", "E"); // should evict c + assertSnapshot(cache, "d", "DD", "e", "E"); + cache.put("f", "F"); + assertSnapshot(cache, "d", "DD", "e", "E", "f", "F"); + cache.put("g", "G"); // should evict d + assertSnapshot(cache, "e", "E", "f", "F", "g", "G"); + cache.put("h", "H"); + assertSnapshot(cache, "e", "E", "f", "F", "g", "G", "h", "H"); + cache.put("i", "III"); // should evict e, f, and g + assertSnapshot(cache, "h", "H", "i", "III"); + cache.put("j", "JJJ"); // should evict h and i + assertSnapshot(cache, "j", "JJJ"); + } + + public void testEvictionThrowsWhenSizesAreInconsistent() { + LruCache cache = new LruCache(4) { + @Override protected int sizeOf(String key, int[] value) { + return value[0]; + } + }; + + int[] a = { 4 }; + cache.put("a", a); + + // get the cache size out of sync + a[0] = 1; + assertEquals(4, cache.size()); + + // evict something + try { + cache.put("b", new int[] { 2 }); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testEvictionThrowsWhenSizesAreNegative() { + LruCache cache = new LruCache(4) { + @Override protected int sizeOf(String key, String value) { + return -1; + } + }; + + try { + cache.put("a", "A"); + fail(); + } catch (IllegalStateException expected) { + } + } + + /** + * Naive caches evict at most one element at a time. This is problematic + * because evicting a small element may be insufficient to make room for a + * large element. + */ + public void testDifferentElementSizes() { + LruCache cache = new LruCache(10) { + @Override protected int sizeOf(String key, String value) { + return value.length(); + } + }; + + cache.put("a", "1"); + cache.put("b", "12345678"); + cache.put("c", "1"); + assertSnapshot(cache, "a", "1", "b", "12345678", "c", "1"); + cache.put("d", "12345678"); // should evict a and b + assertSnapshot(cache, "c", "1", "d", "12345678"); + cache.put("e", "12345678"); // should evict c and d + assertSnapshot(cache, "e", "12345678"); + } + + public void testEvictAll() { + List log = new ArrayList(); + LruCache cache = newRemovalLogCache(log); + cache.put("a", "A"); + cache.put("b", "B"); + cache.put("c", "C"); + cache.evictAll(); + assertEquals(0, cache.size()); + assertEquals(Arrays.asList("a=A", "b=B", "c=C"), log); + } + + public void testEvictAllEvictsSizeZeroElements() { + LruCache cache = new LruCache(10) { + @Override protected int sizeOf(String key, String value) { + return 0; + } + }; + + cache.put("a", "A"); + cache.put("b", "B"); + cache.evictAll(); + assertSnapshot(cache); + } + + public void testRemoveWithCustomSizes() { + LruCache cache = new LruCache(10) { + @Override protected int sizeOf(String key, String value) { + return value.length(); + } + }; + cache.put("a", "123456"); + cache.put("b", "1234"); + cache.remove("a"); + assertEquals(4, cache.size()); + } + + public void testRemoveAbsentElement() { + LruCache cache = new LruCache(10); + cache.put("a", "A"); + cache.put("b", "B"); + assertEquals(null, cache.remove("c")); + assertEquals(2, cache.size()); + } + + public void testRemoveNullThrows() { + LruCache cache = new LruCache(10); + try { + cache.remove(null); + fail(); + } catch (NullPointerException expected) { + } + } + + public void testRemoveCallsEntryRemoved() { + List log = new ArrayList(); + LruCache cache = newRemovalLogCache(log); + cache.put("a", "A"); + cache.remove("a"); + assertEquals(Arrays.asList("a=A>null"), log); + } + + public void testPutCallsEntryRemoved() { + List log = new ArrayList(); + LruCache cache = newRemovalLogCache(log); + cache.put("a", "A"); + cache.put("a", "A2"); + assertEquals(Arrays.asList("a=A>A2"), log); + } + + public void testEntryRemovedIsCalledWithoutSynchronization() { + LruCache cache = new LruCache(3) { + @Override protected void entryRemoved( + boolean evicted, String key, String oldValue, String newValue) { + assertFalse(Thread.holdsLock(this)); + } + }; + + cache.put("a", "A"); + cache.put("a", "A2"); // replaced + cache.put("b", "B"); + cache.put("c", "C"); + cache.put("d", "D"); // single eviction + cache.remove("a"); // removed + cache.evictAll(); // multiple eviction + } + + public void testCreateIsCalledWithoutSynchronization() { + LruCache cache = new LruCache(3) { + @Override protected String create(String key) { + assertFalse(Thread.holdsLock(this)); + return null; + } + }; + + cache.get("a"); + } + + /** + * Test what happens when a value is added to the map while create is + * working. The map value should be returned by get(), and the created value + * should be released with entryRemoved(). + */ + public void testCreateWithConcurrentPut() { + final List log = new ArrayList(); + LruCache cache = new LruCache(3) { + @Override protected String create(String key) { + put(key, "B"); + return "A"; + } + @Override protected void entryRemoved( + boolean evicted, String key, String oldValue, String newValue) { + log.add(key + "=" + oldValue + ">" + newValue); + } + }; + + assertEquals("B", cache.get("a")); + assertEquals(Arrays.asList("a=A>B"), log); + } + + /** + * Test what happens when two creates happen concurrently. The result from + * the first create to return is returned by both gets. The other created + * values should be released with entryRemove(). + */ + public void testCreateWithConcurrentCreate() { + final List log = new ArrayList(); + LruCache cache = new LruCache(3) { + int callCount = 0; + @Override protected Integer create(String key) { + if (callCount++ == 0) { + assertEquals(2, get(key).intValue()); + return 1; + } else { + return 2; + } + } + @Override protected void entryRemoved( + boolean evicted, String key, Integer oldValue, Integer newValue) { + log.add(key + "=" + oldValue + ">" + newValue); + } + }; + + assertEquals(2, cache.get("a").intValue()); + assertEquals(Arrays.asList("a=1>2"), log); + } + + private LruCache newCreatingCache() { + return new LruCache(3) { + @Override protected String create(String key) { + return (key.length() > 1) ? ("created-" + key) : null; + } + }; + } + + private LruCache newRemovalLogCache(final List log) { + return new LruCache(3) { + @Override protected void entryRemoved( + boolean evicted, String key, String oldValue, String newValue) { + String message = evicted + ? (key + "=" + oldValue) + : (key + "=" + oldValue + ">" + newValue); + log.add(message); + } + }; + } + + private void assertHit(LruCache cache, String key, String value) { + assertEquals(value, cache.get(key)); + expectedHitCount++; + assertStatistics(cache); + } + + private void assertMiss(LruCache cache, String key) { + assertEquals(null, cache.get(key)); + expectedMissCount++; + assertStatistics(cache); + } + + private void assertCreated(LruCache cache, String key, String value) { + assertEquals(value, cache.get(key)); + expectedMissCount++; + expectedCreateCount++; + assertStatistics(cache); + } + + private void assertStatistics(LruCache cache) { + assertEquals("create count", expectedCreateCount, cache.createCount()); + assertEquals("put count", expectedPutCount, cache.putCount()); + assertEquals("hit count", expectedHitCount, cache.hitCount()); + assertEquals("miss count", expectedMissCount, cache.missCount()); + assertEquals("eviction count", expectedEvictionCount, cache.evictionCount()); + } + + private void assertSnapshot(LruCache cache, T... keysAndValues) { + List actualKeysAndValues = new ArrayList(); + for (Map.Entry entry : cache.snapshot().entrySet()) { + actualKeysAndValues.add(entry.getKey()); + actualKeysAndValues.add(entry.getValue()); + } + + // assert using lists because order is important for LRUs + assertEquals(Arrays.asList(keysAndValues), actualKeysAndValues); + } +} diff --git a/src/main/java/android/util/MalformedJsonException.java b/src/main/java/android/util/MalformedJsonException.java new file mode 100644 index 0000000..63c19ff --- /dev/null +++ b/src/main/java/android/util/MalformedJsonException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 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 android.util; + +import java.io.IOException; + +/** + * Thrown when a reader encounters malformed JSON. Some syntax errors can be + * ignored by calling {@link JsonReader#setLenient(boolean)}. + */ +public final class MalformedJsonException extends IOException { + private static final long serialVersionUID = 1L; + + public MalformedJsonException(String message) { + super(message); + } +} diff --git a/src/main/java/android/util/MapCollections.java b/src/main/java/android/util/MapCollections.java new file mode 100644 index 0000000..28b788b --- /dev/null +++ b/src/main/java/android/util/MapCollections.java @@ -0,0 +1,559 @@ +/* + * 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 android.util; + +import libcore.util.Objects; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Helper for writing standard Java collection interfaces to a data + * structure like {@link ArrayMap}. + * @hide + */ +abstract class MapCollections { + EntrySet mEntrySet; + KeySet mKeySet; + ValuesCollection mValues; + + final class ArrayIterator implements Iterator { + final int mOffset; + int mSize; + int mIndex; + boolean mCanRemove = false; + + ArrayIterator(int offset) { + mOffset = offset; + mSize = colGetSize(); + } + + @Override + public boolean hasNext() { + return mIndex < mSize; + } + + @Override + public T next() { + Object res = colGetEntry(mIndex, mOffset); + mIndex++; + mCanRemove = true; + return (T)res; + } + + @Override + public void remove() { + if (!mCanRemove) { + throw new IllegalStateException(); + } + mIndex--; + mSize--; + mCanRemove = false; + colRemoveAt(mIndex); + } + } + + final class MapIterator implements Iterator>, Map.Entry { + int mEnd; + int mIndex; + boolean mEntryValid = false; + + MapIterator() { + mEnd = colGetSize() - 1; + mIndex = -1; + } + + @Override + public boolean hasNext() { + return mIndex < mEnd; + } + + @Override + public Map.Entry next() { + mIndex++; + mEntryValid = true; + return this; + } + + @Override + public void remove() { + if (!mEntryValid) { + throw new IllegalStateException(); + } + colRemoveAt(mIndex); + mIndex--; + mEnd--; + mEntryValid = false; + } + + @Override + public K getKey() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return (K)colGetEntry(mIndex, 0); + } + + @Override + public V getValue() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return (V)colGetEntry(mIndex, 1); + } + + @Override + public V setValue(V object) { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return colSetValue(mIndex, object); + } + + @Override + public final boolean equals(Object o) { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + return Objects.equal(e.getKey(), colGetEntry(mIndex, 0)) + && Objects.equal(e.getValue(), colGetEntry(mIndex, 1)); + } + + @Override + public final int hashCode() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + final Object key = colGetEntry(mIndex, 0); + final Object value = colGetEntry(mIndex, 1); + return (key == null ? 0 : key.hashCode()) ^ + (value == null ? 0 : value.hashCode()); + } + + @Override + public final String toString() { + return getKey() + "=" + getValue(); + } + } + + final class EntrySet implements Set> { + @Override + public boolean add(Map.Entry object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection> collection) { + int oldSize = colGetSize(); + for (Map.Entry entry : collection) { + colPut(entry.getKey(), entry.getValue()); + } + return oldSize != colGetSize(); + } + + @Override + public void clear() { + colClear(); + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry) o; + int index = colIndexOfKey(e.getKey()); + if (index < 0) { + return false; + } + Object foundVal = colGetEntry(index, 1); + return Objects.equal(foundVal, e.getValue()); + } + + @Override + public boolean containsAll(Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!contains(it.next())) { + return false; + } + } + return true; + } + + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + + @Override + public Iterator> iterator() { + return new MapIterator(); + } + + @Override + public boolean remove(Object object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + return colGetSize(); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public T[] toArray(T[] array) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object object) { + return equalsSetHelper(this, object); + } + + @Override + public int hashCode() { + int result = 0; + for (int i=colGetSize()-1; i>=0; i--) { + final Object key = colGetEntry(i, 0); + final Object value = colGetEntry(i, 1); + result += ( (key == null ? 0 : key.hashCode()) ^ + (value == null ? 0 : value.hashCode()) ); + } + return result; + } + }; + + final class KeySet implements Set { + + @Override + public boolean add(K object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + colClear(); + } + + @Override + public boolean contains(Object object) { + return colIndexOfKey(object) >= 0; + } + + @Override + public boolean containsAll(Collection collection) { + return containsAllHelper(colGetMap(), collection); + } + + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + + @Override + public Iterator iterator() { + return new ArrayIterator(0); + } + + @Override + public boolean remove(Object object) { + int index = colIndexOfKey(object); + if (index >= 0) { + colRemoveAt(index); + return true; + } + return false; + } + + @Override + public boolean removeAll(Collection collection) { + return removeAllHelper(colGetMap(), collection); + } + + @Override + public boolean retainAll(Collection collection) { + return retainAllHelper(colGetMap(), collection); + } + + @Override + public int size() { + return colGetSize(); + } + + @Override + public Object[] toArray() { + return toArrayHelper(0); + } + + @Override + public T[] toArray(T[] array) { + return toArrayHelper(array, 0); + } + + @Override + public boolean equals(Object object) { + return equalsSetHelper(this, object); + } + + @Override + public int hashCode() { + int result = 0; + for (int i=colGetSize()-1; i>=0; i--) { + Object obj = colGetEntry(i, 0); + result += obj == null ? 0 : obj.hashCode(); + } + return result; + } + }; + + final class ValuesCollection implements Collection { + + @Override + public boolean add(V object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + colClear(); + } + + @Override + public boolean contains(Object object) { + return colIndexOfValue(object) >= 0; + } + + @Override + public boolean containsAll(Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!contains(it.next())) { + return false; + } + } + return true; + } + + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + + @Override + public Iterator iterator() { + return new ArrayIterator(1); + } + + @Override + public boolean remove(Object object) { + int index = colIndexOfValue(object); + if (index >= 0) { + colRemoveAt(index); + return true; + } + return false; + } + + @Override + public boolean removeAll(Collection collection) { + int N = colGetSize(); + boolean changed = false; + for (int i=0; i collection) { + int N = colGetSize(); + boolean changed = false; + for (int i=0; i T[] toArray(T[] array) { + return toArrayHelper(array, 1); + } + }; + + public static boolean containsAllHelper(Map map, Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!map.containsKey(it.next())) { + return false; + } + } + return true; + } + + public static boolean removeAllHelper(Map map, Collection collection) { + int oldSize = map.size(); + Iterator it = collection.iterator(); + while (it.hasNext()) { + map.remove(it.next()); + } + return oldSize != map.size(); + } + + public static boolean retainAllHelper(Map map, Collection collection) { + int oldSize = map.size(); + Iterator it = map.keySet().iterator(); + while (it.hasNext()) { + if (!collection.contains(it.next())) { + it.remove(); + } + } + return oldSize != map.size(); + } + + public Object[] toArrayHelper(int offset) { + final int N = colGetSize(); + Object[] result = new Object[N]; + for (int i=0; i T[] toArrayHelper(T[] array, int offset) { + final int N = colGetSize(); + if (array.length < N) { + @SuppressWarnings("unchecked") T[] newArray + = (T[]) Array.newInstance(array.getClass().getComponentType(), N); + array = newArray; + } + for (int i=0; i N) { + array[N] = null; + } + return array; + } + + public static boolean equalsSetHelper(Set set, Object object) { + if (set == object) { + return true; + } + if (object instanceof Set) { + Set s = (Set) object; + + try { + return set.size() == s.size() && set.containsAll(s); + } catch (NullPointerException ignored) { + return false; + } catch (ClassCastException ignored) { + return false; + } + } + return false; + } + + public Set> getEntrySet() { + if (mEntrySet == null) { + mEntrySet = new EntrySet(); + } + return mEntrySet; + } + + public Set getKeySet() { + if (mKeySet == null) { + mKeySet = new KeySet(); + } + return mKeySet; + } + + public Collection getValues() { + if (mValues == null) { + mValues = new ValuesCollection(); + } + return mValues; + } + + protected abstract int colGetSize(); + protected abstract Object colGetEntry(int index, int offset); + protected abstract int colIndexOfKey(Object key); + protected abstract int colIndexOfValue(Object key); + protected abstract Map colGetMap(); + protected abstract void colPut(K key, V value); + protected abstract V colSetValue(int index, V value); + protected abstract void colRemoveAt(int index); + protected abstract void colClear(); +} diff --git a/src/main/java/android/util/MathUtils.java b/src/main/java/android/util/MathUtils.java new file mode 100644 index 0000000..13a692e --- /dev/null +++ b/src/main/java/android/util/MathUtils.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2009 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 android.util; + +import java.util.Random; + +/** + * A class that contains utility methods related to numbers. + * + * @hide Pending API council approval + */ +public final class MathUtils { + private static final Random sRandom = new Random(); + private static final float DEG_TO_RAD = 3.1415926f / 180.0f; + private static final float RAD_TO_DEG = 180.0f / 3.1415926f; + + private MathUtils() { + } + + public static float abs(float v) { + return v > 0 ? v : -v; + } + + public static int constrain(int amount, int low, int high) { + return amount < low ? low : (amount > high ? high : amount); + } + + public static long constrain(long amount, long low, long high) { + return amount < low ? low : (amount > high ? high : amount); + } + + public static float constrain(float amount, float low, float high) { + return amount < low ? low : (amount > high ? high : amount); + } + + public static float log(float a) { + return (float) Math.log(a); + } + + public static float exp(float a) { + return (float) Math.exp(a); + } + + public static float pow(float a, float b) { + return (float) Math.pow(a, b); + } + + public static float max(float a, float b) { + return a > b ? a : b; + } + + public static float max(int a, int b) { + return a > b ? a : b; + } + + public static float max(float a, float b, float c) { + return a > b ? (a > c ? a : c) : (b > c ? b : c); + } + + public static float max(int a, int b, int c) { + return a > b ? (a > c ? a : c) : (b > c ? b : c); + } + + public static float min(float a, float b) { + return a < b ? a : b; + } + + public static float min(int a, int b) { + return a < b ? a : b; + } + + public static float min(float a, float b, float c) { + return a < b ? (a < c ? a : c) : (b < c ? b : c); + } + + public static float min(int a, int b, int c) { + return a < b ? (a < c ? a : c) : (b < c ? b : c); + } + + public static float dist(float x1, float y1, float x2, float y2) { + final float x = (x2 - x1); + final float y = (y2 - y1); + return (float) Math.sqrt(x * x + y * y); + } + + public static float dist(float x1, float y1, float z1, float x2, float y2, float z2) { + final float x = (x2 - x1); + final float y = (y2 - y1); + final float z = (z2 - z1); + return (float) Math.sqrt(x * x + y * y + z * z); + } + + public static float mag(float a, float b) { + return (float) Math.sqrt(a * a + b * b); + } + + public static float mag(float a, float b, float c) { + return (float) Math.sqrt(a * a + b * b + c * c); + } + + public static float sq(float v) { + return v * v; + } + + public static float radians(float degrees) { + return degrees * DEG_TO_RAD; + } + + public static float degrees(float radians) { + return radians * RAD_TO_DEG; + } + + public static float acos(float value) { + return (float) Math.acos(value); + } + + public static float asin(float value) { + return (float) Math.asin(value); + } + + public static float atan(float value) { + return (float) Math.atan(value); + } + + public static float atan2(float a, float b) { + return (float) Math.atan2(a, b); + } + + public static float tan(float angle) { + return (float) Math.tan(angle); + } + + public static float lerp(float start, float stop, float amount) { + return start + (stop - start) * amount; + } + + public static float norm(float start, float stop, float value) { + return (value - start) / (stop - start); + } + + public static float map(float minStart, float minStop, float maxStart, float maxStop, float value) { + return maxStart + (maxStart - maxStop) * ((value - minStart) / (minStop - minStart)); + } + + public static int random(int howbig) { + return (int) (sRandom.nextFloat() * howbig); + } + + public static int random(int howsmall, int howbig) { + if (howsmall >= howbig) return howsmall; + return (int) (sRandom.nextFloat() * (howbig - howsmall) + howsmall); + } + + public static float random(float howbig) { + return sRandom.nextFloat() * howbig; + } + + public static float random(float howsmall, float howbig) { + if (howsmall >= howbig) return howsmall; + return sRandom.nextFloat() * (howbig - howsmall) + howsmall; + } + + public static void randomSeed(long seed) { + sRandom.setSeed(seed); + } +} diff --git a/src/main/java/android/util/MonthDisplayHelper.java b/src/main/java/android/util/MonthDisplayHelper.java new file mode 100644 index 0000000..c3f13fc --- /dev/null +++ b/src/main/java/android/util/MonthDisplayHelper.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2007 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 android.util; + +import java.util.Calendar; + +/** + * Helps answer common questions that come up when displaying a month in a + * 6 row calendar grid format. + * + * Not thread safe. + */ +public class MonthDisplayHelper { + + // display pref + private final int mWeekStartDay; + + // holds current month, year, helps compute display + private Calendar mCalendar; + + // cached computed stuff that helps with display + private int mNumDaysInMonth; + private int mNumDaysInPrevMonth; + private int mOffset; + + + /** + * @param year The year. + * @param month The month. + * @param weekStartDay What day of the week the week should start. + */ + public MonthDisplayHelper(int year, int month, int weekStartDay) { + + if (weekStartDay < Calendar.SUNDAY || weekStartDay > Calendar.SATURDAY) { + throw new IllegalArgumentException(); + } + mWeekStartDay = weekStartDay; + + mCalendar = Calendar.getInstance(); + mCalendar.set(Calendar.YEAR, year); + mCalendar.set(Calendar.MONTH, month); + mCalendar.set(Calendar.DAY_OF_MONTH, 1); + mCalendar.set(Calendar.HOUR_OF_DAY, 0); + mCalendar.set(Calendar.MINUTE, 0); + mCalendar.set(Calendar.SECOND, 0); + mCalendar.getTimeInMillis(); + + recalculate(); + } + + + public MonthDisplayHelper(int year, int month) { + this(year, month, Calendar.SUNDAY); + } + + + public int getYear() { + return mCalendar.get(Calendar.YEAR); + } + + public int getMonth() { + return mCalendar.get(Calendar.MONTH); + } + + + public int getWeekStartDay() { + return mWeekStartDay; + } + + /** + * @return The first day of the month using a constants such as + * {@link java.util.Calendar#SUNDAY}. + */ + public int getFirstDayOfMonth() { + return mCalendar.get(Calendar.DAY_OF_WEEK); + } + + /** + * @return The number of days in the month. + */ + public int getNumberOfDaysInMonth() { + return mNumDaysInMonth; + } + + + /** + * @return The offset from displaying everything starting on the very first + * box. For example, if the calendar is set to display the first day of + * the week as Sunday, and the month starts on a Wednesday, the offset is 3. + */ + public int getOffset() { + return mOffset; + } + + + /** + * @param row Which row (0-5). + * @return the digits of the month to display in one + * of the 6 rows of a calendar month display. + */ + public int[] getDigitsForRow(int row) { + if (row < 0 || row > 5) { + throw new IllegalArgumentException("row " + row + + " out of range (0-5)"); + } + + int [] result = new int[7]; + for (int column = 0; column < 7; column++) { + result[column] = getDayAt(row, column); + } + + return result; + } + + /** + * @param row The row, 0-5, starting from the top. + * @param column The column, 0-6, starting from the left. + * @return The day at a particular row, column + */ + public int getDayAt(int row, int column) { + + if (row == 0 && column < mOffset) { + return mNumDaysInPrevMonth + column - mOffset + 1; + } + + int day = 7 * row + column - mOffset + 1; + + return (day > mNumDaysInMonth) ? + day - mNumDaysInMonth : day; + } + + /** + * @return Which row day is in. + */ + public int getRowOf(int day) { + return (day + mOffset - 1) / 7; + } + + /** + * @return Which column day is in. + */ + public int getColumnOf(int day) { + return (day + mOffset - 1) % 7; + } + + /** + * Decrement the month. + */ + public void previousMonth() { + mCalendar.add(Calendar.MONTH, -1); + recalculate(); + } + + /** + * Increment the month. + */ + public void nextMonth() { + mCalendar.add(Calendar.MONTH, 1); + recalculate(); + } + + /** + * @return Whether the row and column fall within the month. + */ + public boolean isWithinCurrentMonth(int row, int column) { + + if (row < 0 || column < 0 || row > 5 || column > 6) { + return false; + } + + if (row == 0 && column < mOffset) { + return false; + } + + int day = 7 * row + column - mOffset + 1; + if (day > mNumDaysInMonth) { + return false; + } + return true; + } + + + // helper method that recalculates cached values based on current month / year + private void recalculate() { + + mNumDaysInMonth = mCalendar.getActualMaximum(Calendar.DAY_OF_MONTH); + + mCalendar.add(Calendar.MONTH, -1); + mNumDaysInPrevMonth = mCalendar.getActualMaximum(Calendar.DAY_OF_MONTH); + mCalendar.add(Calendar.MONTH, 1); + + int firstDayOfMonth = getFirstDayOfMonth(); + int offset = firstDayOfMonth - mWeekStartDay; + if (offset < 0) { + offset += 7; + } + mOffset = offset; + } +} diff --git a/src/main/java/android/util/MonthDisplayHelperTest.java b/src/main/java/android/util/MonthDisplayHelperTest.java new file mode 100644 index 0000000..55da665 --- /dev/null +++ b/src/main/java/android/util/MonthDisplayHelperTest.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2007 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 android.util; + +import android.test.suitebuilder.annotation.SmallTest; + +import java.util.Calendar; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link MonthDisplayHelper}. + */ +public class MonthDisplayHelperTest extends TestCase { + + + @SmallTest + public void testFirstDayOfMonth() { + + assertEquals("august 2007", + Calendar.WEDNESDAY, + new MonthDisplayHelper(2007, Calendar.AUGUST).getFirstDayOfMonth()); + + assertEquals("september, 2007", + Calendar.SATURDAY, + new MonthDisplayHelper(2007, Calendar.SEPTEMBER).getFirstDayOfMonth()); + } + + @SmallTest + public void testNumberOfDaysInCurrentMonth() { + assertEquals(30, + new MonthDisplayHelper(2007, Calendar.SEPTEMBER).getNumberOfDaysInMonth()); + } + + @SmallTest + public void testMonthRows() { + MonthDisplayHelper helper = new MonthDisplayHelper(2007, Calendar.SEPTEMBER); + + assertArraysEqual(new int[]{26, 27, 28, 29, 30, 31, 1}, + helper.getDigitsForRow(0)); + assertArraysEqual(new int[]{2, 3, 4, 5, 6, 7, 8}, + helper.getDigitsForRow(1)); + assertArraysEqual(new int[]{30, 1, 2, 3, 4, 5, 6}, + helper.getDigitsForRow(5)); + + } + + @SmallTest + public void testMonthRowsWeekStartsMonday() { + MonthDisplayHelper helper = new MonthDisplayHelper(2007, + Calendar.SEPTEMBER, Calendar.MONDAY); + + assertArraysEqual(new int[]{27, 28, 29, 30, 31, 1, 2}, + helper.getDigitsForRow(0)); + assertArraysEqual(new int[]{3, 4, 5, 6, 7, 8, 9}, + helper.getDigitsForRow(1)); + assertArraysEqual(new int[]{24, 25, 26, 27, 28, 29, 30}, + helper.getDigitsForRow(4)); + assertArraysEqual(new int[]{1, 2, 3, 4, 5, 6, 7}, + helper.getDigitsForRow(5)); + } + + @SmallTest + public void testMonthRowsWeekStartsSaturday() { + MonthDisplayHelper helper = new MonthDisplayHelper(2007, + Calendar.SEPTEMBER, Calendar.SATURDAY); + + assertArraysEqual(new int[]{1, 2, 3, 4, 5, 6, 7}, + helper.getDigitsForRow(0)); + assertArraysEqual(new int[]{8, 9, 10, 11, 12, 13, 14}, + helper.getDigitsForRow(1)); + assertArraysEqual(new int[]{29, 30, 1, 2, 3, 4, 5}, + helper.getDigitsForRow(4)); + + + helper = new MonthDisplayHelper(2007, + Calendar.AUGUST, Calendar.SATURDAY); + + assertArraysEqual(new int[]{28, 29, 30, 31, 1, 2, 3}, + helper.getDigitsForRow(0)); + assertArraysEqual(new int[]{4, 5, 6, 7, 8, 9, 10}, + helper.getDigitsForRow(1)); + assertArraysEqual(new int[]{25, 26, 27, 28, 29, 30, 31}, + helper.getDigitsForRow(4)); + } + + @SmallTest + public void testGetDayAt() { + MonthDisplayHelper helper = new MonthDisplayHelper(2007, + Calendar.SEPTEMBER, Calendar.SUNDAY); + + assertEquals(26, helper.getDayAt(0, 0)); + assertEquals(1, helper.getDayAt(0, 6)); + assertEquals(17, helper.getDayAt(3, 1)); + assertEquals(2, helper.getDayAt(5, 2)); + } + + @SmallTest + public void testPrevMonth() { + MonthDisplayHelper helper = new MonthDisplayHelper(2007, + Calendar.SEPTEMBER, Calendar.SUNDAY); + + assertArraysEqual(new int[]{26, 27, 28, 29, 30, 31, 1}, + helper.getDigitsForRow(0)); + + helper.previousMonth(); + + assertEquals(Calendar.AUGUST, helper.getMonth()); + assertArraysEqual(new int[]{29, 30, 31, 1, 2, 3, 4}, + helper.getDigitsForRow(0)); + } + + @SmallTest + public void testPrevMonthRollOver() { + MonthDisplayHelper helper = new MonthDisplayHelper(2007, + Calendar.JANUARY); + + helper.previousMonth(); + + assertEquals(2006, helper.getYear()); + assertEquals(Calendar.DECEMBER, helper.getMonth()); + } + + @SmallTest + public void testNextMonth() { + MonthDisplayHelper helper = new MonthDisplayHelper(2007, + Calendar.AUGUST, Calendar.SUNDAY); + + assertArraysEqual(new int[]{29, 30, 31, 1, 2, 3, 4}, + helper.getDigitsForRow(0)); + + helper.nextMonth(); + + assertEquals(Calendar.SEPTEMBER, helper.getMonth()); + assertArraysEqual(new int[]{26, 27, 28, 29, 30, 31, 1}, + helper.getDigitsForRow(0)); + } + + @SmallTest + public void testGetRowOf() { + MonthDisplayHelper helper = new MonthDisplayHelper(2007, + Calendar.AUGUST, Calendar.SUNDAY); + + assertEquals(0, helper.getRowOf(2)); + assertEquals(0, helper.getRowOf(4)); + assertEquals(2, helper.getRowOf(12)); + assertEquals(2, helper.getRowOf(18)); + assertEquals(3, helper.getRowOf(19)); + } + + @SmallTest + public void testGetColumnOf() { + MonthDisplayHelper helper = new MonthDisplayHelper(2007, + Calendar.AUGUST, Calendar.SUNDAY); + + assertEquals(3, helper.getColumnOf(1)); + assertEquals(4, helper.getColumnOf(9)); + assertEquals(5, helper.getColumnOf(17)); + assertEquals(6, helper.getColumnOf(25)); + assertEquals(0, helper.getColumnOf(26)); + } + + @SmallTest + public void testWithinCurrentMonth() { + MonthDisplayHelper helper = new MonthDisplayHelper(2007, + Calendar.SEPTEMBER, Calendar.SUNDAY); + + // out of bounds + assertFalse(helper.isWithinCurrentMonth(-1, 3)); + assertFalse(helper.isWithinCurrentMonth(6, 3)); + assertFalse(helper.isWithinCurrentMonth(2, -1)); + assertFalse(helper.isWithinCurrentMonth(2, 7)); + + // last day of previous month + assertFalse(helper.isWithinCurrentMonth(0, 5)); + + // first day of next month + assertFalse(helper.isWithinCurrentMonth(5, 1)); + + // first day in month + assertTrue(helper.isWithinCurrentMonth(0, 6)); + + // last day in month + assertTrue(helper.isWithinCurrentMonth(5, 0)); + } + + private void assertArraysEqual(int[] expected, int[] actual) { + assertEquals("array length", expected.length, actual.length); + for (int i = 0; i < expected.length; i++) { + assertEquals("index " + i, + expected[i], actual[i]); + } + } + +} diff --git a/src/main/java/android/util/MutableBoolean.java b/src/main/java/android/util/MutableBoolean.java new file mode 100644 index 0000000..5a8a200 --- /dev/null +++ b/src/main/java/android/util/MutableBoolean.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 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 android.util; + +/** + */ +public final class MutableBoolean { + public boolean value; + + public MutableBoolean(boolean value) { + this.value = value; + } +} diff --git a/src/main/java/android/util/MutableByte.java b/src/main/java/android/util/MutableByte.java new file mode 100644 index 0000000..7397ba4 --- /dev/null +++ b/src/main/java/android/util/MutableByte.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 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 android.util; + +/** + */ +public final class MutableByte { + public byte value; + + public MutableByte(byte value) { + this.value = value; + } +} diff --git a/src/main/java/android/util/MutableChar.java b/src/main/java/android/util/MutableChar.java new file mode 100644 index 0000000..f435331 --- /dev/null +++ b/src/main/java/android/util/MutableChar.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 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 android.util; + +/** + */ +public final class MutableChar { + public char value; + + public MutableChar(char value) { + this.value = value; + } +} diff --git a/src/main/java/android/util/MutableDouble.java b/src/main/java/android/util/MutableDouble.java new file mode 100644 index 0000000..f62f47e --- /dev/null +++ b/src/main/java/android/util/MutableDouble.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 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 android.util; + +/** + */ +public final class MutableDouble { + public double value; + + public MutableDouble(double value) { + this.value = value; + } +} diff --git a/src/main/java/android/util/MutableFloat.java b/src/main/java/android/util/MutableFloat.java new file mode 100644 index 0000000..6b5441c --- /dev/null +++ b/src/main/java/android/util/MutableFloat.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 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 android.util; + +/** + */ +public final class MutableFloat { + public float value; + + public MutableFloat(float value) { + this.value = value; + } +} diff --git a/src/main/java/android/util/MutableInt.java b/src/main/java/android/util/MutableInt.java new file mode 100644 index 0000000..2f93030 --- /dev/null +++ b/src/main/java/android/util/MutableInt.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 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 android.util; + +/** + */ +public final class MutableInt { + public int value; + + public MutableInt(int value) { + this.value = value; + } +} diff --git a/src/main/java/android/util/MutableLong.java b/src/main/java/android/util/MutableLong.java new file mode 100644 index 0000000..94beab5 --- /dev/null +++ b/src/main/java/android/util/MutableLong.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 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 android.util; + +/** + */ +public final class MutableLong { + public long value; + + public MutableLong(long value) { + this.value = value; + } +} diff --git a/src/main/java/android/util/MutableShort.java b/src/main/java/android/util/MutableShort.java new file mode 100644 index 0000000..cdd9923 --- /dev/null +++ b/src/main/java/android/util/MutableShort.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 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 android.util; + +/** + */ +public final class MutableShort { + public short value; + + public MutableShort(short value) { + this.value = value; + } +} diff --git a/src/main/java/android/util/NoSuchPropertyException.java b/src/main/java/android/util/NoSuchPropertyException.java new file mode 100644 index 0000000..b93f983 --- /dev/null +++ b/src/main/java/android/util/NoSuchPropertyException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 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 android.util; + +/** + * Thrown when code requests a {@link Property} on a class that does + * not expose the appropriate method or field. + * + * @see Property#of(java.lang.Class, java.lang.Class, java.lang.String) + */ +public class NoSuchPropertyException extends RuntimeException { + + public NoSuchPropertyException(String s) { + super(s); + } + +} diff --git a/src/main/java/android/util/NtpTrustedTime.java b/src/main/java/android/util/NtpTrustedTime.java new file mode 100644 index 0000000..8ebcacd --- /dev/null +++ b/src/main/java/android/util/NtpTrustedTime.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2011 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 android.util; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.SntpClient; +import android.os.SystemClock; +import android.provider.Settings; + +/** + * {@link TrustedTime} that connects with a remote NTP server as its trusted + * time source. + * + * @hide + */ +public class NtpTrustedTime implements TrustedTime { + private static final String TAG = "NtpTrustedTime"; + private static final boolean LOGD = false; + + private static NtpTrustedTime sSingleton; + private static Context sContext; + + private final String mServer; + private final long mTimeout; + + private ConnectivityManager mCM; + + private boolean mHasCache; + private long mCachedNtpTime; + private long mCachedNtpElapsedRealtime; + private long mCachedNtpCertainty; + + private NtpTrustedTime(String server, long timeout) { + if (LOGD) Log.d(TAG, "creating NtpTrustedTime using " + server); + mServer = server; + mTimeout = timeout; + } + + public static synchronized NtpTrustedTime getInstance(Context context) { + if (sSingleton == null) { + final Resources res = context.getResources(); + final ContentResolver resolver = context.getContentResolver(); + + final String defaultServer = res.getString( + com.android.internal.R.string.config_ntpServer); + final long defaultTimeout = res.getInteger( + com.android.internal.R.integer.config_ntpTimeout); + + final String secureServer = Settings.Global.getString( + resolver, Settings.Global.NTP_SERVER); + final long timeout = Settings.Global.getLong( + resolver, Settings.Global.NTP_TIMEOUT, defaultTimeout); + + final String server = secureServer != null ? secureServer : defaultServer; + sSingleton = new NtpTrustedTime(server, timeout); + sContext = context; + } + + return sSingleton; + } + + @Override + public boolean forceRefresh() { + if (mServer == null) { + // missing server, so no trusted time available + return false; + } + + // We can't do this at initialization time: ConnectivityService might not be running yet. + synchronized (this) { + if (mCM == null) { + mCM = (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE); + } + } + + final NetworkInfo ni = mCM == null ? null : mCM.getActiveNetworkInfo(); + if (ni == null || !ni.isConnected()) { + if (LOGD) Log.d(TAG, "forceRefresh: no connectivity"); + return false; + } + + + if (LOGD) Log.d(TAG, "forceRefresh() from cache miss"); + final SntpClient client = new SntpClient(); + if (client.requestTime(mServer, (int) mTimeout)) { + mHasCache = true; + mCachedNtpTime = client.getNtpTime(); + mCachedNtpElapsedRealtime = client.getNtpTimeReference(); + mCachedNtpCertainty = client.getRoundTripTime() / 2; + return true; + } else { + return false; + } + } + + @Override + public boolean hasCache() { + return mHasCache; + } + + @Override + public long getCacheAge() { + if (mHasCache) { + return SystemClock.elapsedRealtime() - mCachedNtpElapsedRealtime; + } else { + return Long.MAX_VALUE; + } + } + + @Override + public long getCacheCertainty() { + if (mHasCache) { + return mCachedNtpCertainty; + } else { + return Long.MAX_VALUE; + } + } + + @Override + public long currentTimeMillis() { + if (!mHasCache) { + throw new IllegalStateException("Missing authoritative time source"); + } + if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit"); + + // current time is age after the last ntp cache; callers who + // want fresh values will hit makeAuthoritative() first. + return mCachedNtpTime + getCacheAge(); + } + + public long getCachedNtpTime() { + if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit"); + return mCachedNtpTime; + } + + public long getCachedNtpTimeReference() { + return mCachedNtpElapsedRealtime; + } +} diff --git a/src/main/java/android/util/Pair.java b/src/main/java/android/util/Pair.java new file mode 100644 index 0000000..6027d08 --- /dev/null +++ b/src/main/java/android/util/Pair.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2009 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 android.util; + +import libcore.util.Objects; + +/** + * Container to ease passing around a tuple of two objects. This object provides a sensible + * implementation of equals(), returning true if equals() is true on each of the contained + * objects. + */ +public class Pair { + public final F first; + public final S second; + + /** + * Constructor for a Pair. + * + * @param first the first object in the Pair + * @param second the second object in the pair + */ + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + /** + * Checks the two objects for equality by delegating to their respective + * {@link Object#equals(Object)} methods. + * + * @param o the {@link Pair} to which this one is to be checked for equality + * @return true if the underlying objects of the Pair are both considered + * equal + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof Pair)) { + return false; + } + Pair p = (Pair) o; + return Objects.equal(p.first, first) && Objects.equal(p.second, second); + } + + /** + * Compute a hash code using the hash codes of the underlying objects + * + * @return a hashcode of the Pair + */ + @Override + public int hashCode() { + return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode()); + } + + /** + * Convenience method for creating an appropriately typed pair. + * @param a the first object in the Pair + * @param b the second object in the pair + * @return a Pair that is templatized with the types of a and b + */ + public static Pair create(A a, B b) { + return new Pair(a, b); + } +} diff --git a/src/main/java/android/util/PathParser.java b/src/main/java/android/util/PathParser.java new file mode 100644 index 0000000..92b19be --- /dev/null +++ b/src/main/java/android/util/PathParser.java @@ -0,0 +1,690 @@ +/* + * 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 android.util; + +import android.graphics.Path; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * @hide + */ +public class PathParser { + static final String LOGTAG = PathParser.class.getSimpleName(); + + /** + * @param pathData The string representing a path, the same as "d" string in svg file. + * @return the generated Path object. + */ + public static Path createPathFromPathData(String pathData) { + Path path = new Path(); + PathDataNode[] nodes = createNodesFromPathData(pathData); + if (nodes != null) { + try { + PathDataNode.nodesToPath(nodes, path); + } catch (RuntimeException e) { + throw new RuntimeException("Error in parsing " + pathData, e); + } + return path; + } + return null; + } + + /** + * @param pathData The string representing a path, the same as "d" string in svg file. + * @return an array of the PathDataNode. + */ + public static PathDataNode[] createNodesFromPathData(String pathData) { + if (pathData == null) { + return null; + } + int start = 0; + int end = 1; + + ArrayList list = new ArrayList(); + while (end < pathData.length()) { + end = nextStart(pathData, end); + String s = pathData.substring(start, end).trim(); + if (s.length() > 0) { + float[] val = getFloats(s); + addNode(list, s.charAt(0), val); + } + + start = end; + end++; + } + if ((end - start) == 1 && start < pathData.length()) { + addNode(list, pathData.charAt(start), new float[0]); + } + return list.toArray(new PathDataNode[list.size()]); + } + + /** + * @param source The array of PathDataNode to be duplicated. + * @return a deep copy of the source. + */ + public static PathDataNode[] deepCopyNodes(PathDataNode[] source) { + if (source == null) { + return null; + } + PathDataNode[] copy = new PathParser.PathDataNode[source.length]; + for (int i = 0; i < source.length; i ++) { + copy[i] = new PathDataNode(source[i]); + } + return copy; + } + + /** + * @param nodesFrom The source path represented in an array of PathDataNode + * @param nodesTo The target path represented in an array of PathDataNode + * @return whether the nodesFrom can morph into nodesTo + */ + public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) { + if (nodesFrom == null || nodesTo == null) { + return false; + } + + if (nodesFrom.length != nodesTo.length) { + return false; + } + + for (int i = 0; i < nodesFrom.length; i ++) { + if (nodesFrom[i].mType != nodesTo[i].mType + || nodesFrom[i].mParams.length != nodesTo[i].mParams.length) { + return false; + } + } + return true; + } + + /** + * Update the target's data to match the source. + * Before calling this, make sure canMorph(target, source) is true. + * + * @param target The target path represented in an array of PathDataNode + * @param source The source path represented in an array of PathDataNode + */ + public static void updateNodes(PathDataNode[] target, PathDataNode[] source) { + for (int i = 0; i < source.length; i ++) { + target[i].mType = source[i].mType; + for (int j = 0; j < source[i].mParams.length; j ++) { + target[i].mParams[j] = source[i].mParams[j]; + } + } + } + + private static int nextStart(String s, int end) { + char c; + + while (end < s.length()) { + c = s.charAt(end); + // Note that 'e' or 'E' are not valid path commands, but could be + // used for floating point numbers' scientific notation. + // Therefore, when searching for next command, we should ignore 'e' + // and 'E'. + if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0)) + && c != 'e' && c != 'E') { + return end; + } + end++; + } + return end; + } + + private static void addNode(ArrayList list, char cmd, float[] val) { + list.add(new PathDataNode(cmd, val)); + } + + private static class ExtractFloatResult { + // We need to return the position of the next separator and whether the + // next float starts with a '-' or a '.'. + int mEndPosition; + boolean mEndWithNegOrDot; + } + + /** + * Parse the floats in the string. + * This is an optimized version of parseFloat(s.split(",|\\s")); + * + * @param s the string containing a command and list of floats + * @return array of floats + */ + private static float[] getFloats(String s) { + if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') { + return new float[0]; + } + try { + float[] results = new float[s.length()]; + int count = 0; + int startPosition = 1; + int endPosition = 0; + + ExtractFloatResult result = new ExtractFloatResult(); + int totalLength = s.length(); + + // The startPosition should always be the first character of the + // current number, and endPosition is the character after the current + // number. + while (startPosition < totalLength) { + extract(s, startPosition, result); + endPosition = result.mEndPosition; + + if (startPosition < endPosition) { + results[count++] = Float.parseFloat( + s.substring(startPosition, endPosition)); + } + + if (result.mEndWithNegOrDot) { + // Keep the '-' or '.' sign with next number. + startPosition = endPosition; + } else { + startPosition = endPosition + 1; + } + } + return Arrays.copyOf(results, count); + } catch (NumberFormatException e) { + throw new RuntimeException("error in parsing \"" + s + "\"", e); + } + } + + /** + * Calculate the position of the next comma or space or negative sign + * @param s the string to search + * @param start the position to start searching + * @param result the result of the extraction, including the position of the + * the starting position of next number, whether it is ending with a '-'. + */ + private static void extract(String s, int start, ExtractFloatResult result) { + // Now looking for ' ', ',', '.' or '-' from the start. + int currentIndex = start; + boolean foundSeparator = false; + result.mEndWithNegOrDot = false; + boolean secondDot = false; + boolean isExponential = false; + for (; currentIndex < s.length(); currentIndex++) { + boolean isPrevExponential = isExponential; + isExponential = false; + char currentChar = s.charAt(currentIndex); + switch (currentChar) { + case ' ': + case ',': + foundSeparator = true; + break; + case '-': + // The negative sign following a 'e' or 'E' is not a separator. + if (currentIndex != start && !isPrevExponential) { + foundSeparator = true; + result.mEndWithNegOrDot = true; + } + break; + case '.': + if (!secondDot) { + secondDot = true; + } else { + // This is the second dot, and it is considered as a separator. + foundSeparator = true; + result.mEndWithNegOrDot = true; + } + break; + case 'e': + case 'E': + isExponential = true; + break; + } + if (foundSeparator) { + break; + } + } + // When there is nothing found, then we put the end position to the end + // of the string. + result.mEndPosition = currentIndex; + } + + /** + * Each PathDataNode represents one command in the "d" attribute of the svg + * file. + * An array of PathDataNode can represent the whole "d" attribute. + */ + public static class PathDataNode { + private char mType; + private float[] mParams; + + private PathDataNode(char type, float[] params) { + mType = type; + mParams = params; + } + + private PathDataNode(PathDataNode n) { + mType = n.mType; + mParams = Arrays.copyOf(n.mParams, n.mParams.length); + } + + /** + * Convert an array of PathDataNode to Path. + * + * @param node The source array of PathDataNode. + * @param path The target Path object. + */ + public static void nodesToPath(PathDataNode[] node, Path path) { + float[] current = new float[6]; + char previousCommand = 'm'; + for (int i = 0; i < node.length; i++) { + addCommand(path, current, previousCommand, node[i].mType, node[i].mParams); + previousCommand = node[i].mType; + } + } + + /** + * The current PathDataNode will be interpolated between the + * nodeFrom and nodeTo according to the + * fraction. + * + * @param nodeFrom The start value as a PathDataNode. + * @param nodeTo The end value as a PathDataNode + * @param fraction The fraction to interpolate. + */ + public void interpolatePathDataNode(PathDataNode nodeFrom, + PathDataNode nodeTo, float fraction) { + for (int i = 0; i < nodeFrom.mParams.length; i++) { + mParams[i] = nodeFrom.mParams[i] * (1 - fraction) + + nodeTo.mParams[i] * fraction; + } + } + + private static void addCommand(Path path, float[] current, + char previousCmd, char cmd, float[] val) { + + int incr = 2; + float currentX = current[0]; + float currentY = current[1]; + float ctrlPointX = current[2]; + float ctrlPointY = current[3]; + float currentSegmentStartX = current[4]; + float currentSegmentStartY = current[5]; + float reflectiveCtrlPointX; + float reflectiveCtrlPointY; + + switch (cmd) { + case 'z': + case 'Z': + path.close(); + // Path is closed here, but we need to move the pen to the + // closed position. So we cache the segment's starting position, + // and restore it here. + currentX = currentSegmentStartX; + currentY = currentSegmentStartY; + ctrlPointX = currentSegmentStartX; + ctrlPointY = currentSegmentStartY; + path.moveTo(currentX, currentY); + break; + case 'm': + case 'M': + case 'l': + case 'L': + case 't': + case 'T': + incr = 2; + break; + case 'h': + case 'H': + case 'v': + case 'V': + incr = 1; + break; + case 'c': + case 'C': + incr = 6; + break; + case 's': + case 'S': + case 'q': + case 'Q': + incr = 4; + break; + case 'a': + case 'A': + incr = 7; + break; + } + + for (int k = 0; k < val.length; k += incr) { + switch (cmd) { + case 'm': // moveto - Start a new sub-path (relative) + path.rMoveTo(val[k + 0], val[k + 1]); + currentX += val[k + 0]; + currentY += val[k + 1]; + currentSegmentStartX = currentX; + currentSegmentStartY = currentY; + break; + case 'M': // moveto - Start a new sub-path + path.moveTo(val[k + 0], val[k + 1]); + currentX = val[k + 0]; + currentY = val[k + 1]; + currentSegmentStartX = currentX; + currentSegmentStartY = currentY; + break; + case 'l': // lineto - Draw a line from the current point (relative) + path.rLineTo(val[k + 0], val[k + 1]); + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'L': // lineto - Draw a line from the current point + path.lineTo(val[k + 0], val[k + 1]); + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'h': // horizontal lineto - Draws a horizontal line (relative) + path.rLineTo(val[k + 0], 0); + currentX += val[k + 0]; + break; + case 'H': // horizontal lineto - Draws a horizontal line + path.lineTo(val[k + 0], currentY); + currentX = val[k + 0]; + break; + case 'v': // vertical lineto - Draws a vertical line from the current point (r) + path.rLineTo(0, val[k + 0]); + currentY += val[k + 0]; + break; + case 'V': // vertical lineto - Draws a vertical line from the current point + path.lineTo(currentX, val[k + 0]); + currentY = val[k + 0]; + break; + case 'c': // curveto - Draws a cubic Bézier curve (relative) + path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], + val[k + 4], val[k + 5]); + + ctrlPointX = currentX + val[k + 2]; + ctrlPointY = currentY + val[k + 3]; + currentX += val[k + 4]; + currentY += val[k + 5]; + + break; + case 'C': // curveto - Draws a cubic Bézier curve + path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], + val[k + 4], val[k + 5]); + currentX = val[k + 4]; + currentY = val[k + 5]; + ctrlPointX = val[k + 2]; + ctrlPointY = val[k + 3]; + break; + case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp) + reflectiveCtrlPointX = 0; + reflectiveCtrlPointY = 0; + if (previousCmd == 'c' || previousCmd == 's' + || previousCmd == 'C' || previousCmd == 'S') { + reflectiveCtrlPointX = currentX - ctrlPointX; + reflectiveCtrlPointY = currentY - ctrlPointY; + } + path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1], + val[k + 2], val[k + 3]); + + ctrlPointX = currentX + val[k + 0]; + ctrlPointY = currentY + val[k + 1]; + currentX += val[k + 2]; + currentY += val[k + 3]; + break; + case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp) + reflectiveCtrlPointX = currentX; + reflectiveCtrlPointY = currentY; + if (previousCmd == 'c' || previousCmd == 's' + || previousCmd == 'C' || previousCmd == 'S') { + reflectiveCtrlPointX = 2 * currentX - ctrlPointX; + reflectiveCtrlPointY = 2 * currentY - ctrlPointY; + } + path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = val[k + 0]; + ctrlPointY = val[k + 1]; + currentX = val[k + 2]; + currentY = val[k + 3]; + break; + case 'q': // Draws a quadratic Bézier (relative) + path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = currentX + val[k + 0]; + ctrlPointY = currentY + val[k + 1]; + currentX += val[k + 2]; + currentY += val[k + 3]; + break; + case 'Q': // Draws a quadratic Bézier + path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = val[k + 0]; + ctrlPointY = val[k + 1]; + currentX = val[k + 2]; + currentY = val[k + 3]; + break; + case 't': // Draws a quadratic Bézier curve(reflective control point)(relative) + reflectiveCtrlPointX = 0; + reflectiveCtrlPointY = 0; + if (previousCmd == 'q' || previousCmd == 't' + || previousCmd == 'Q' || previousCmd == 'T') { + reflectiveCtrlPointX = currentX - ctrlPointX; + reflectiveCtrlPointY = currentY - ctrlPointY; + } + path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1]); + ctrlPointX = currentX + reflectiveCtrlPointX; + ctrlPointY = currentY + reflectiveCtrlPointY; + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'T': // Draws a quadratic Bézier curve (reflective control point) + reflectiveCtrlPointX = currentX; + reflectiveCtrlPointY = currentY; + if (previousCmd == 'q' || previousCmd == 't' + || previousCmd == 'Q' || previousCmd == 'T') { + reflectiveCtrlPointX = 2 * currentX - ctrlPointX; + reflectiveCtrlPointY = 2 * currentY - ctrlPointY; + } + path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1]); + ctrlPointX = reflectiveCtrlPointX; + ctrlPointY = reflectiveCtrlPointY; + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'a': // Draws an elliptical arc + // (rx ry x-axis-rotation large-arc-flag sweep-flag x y) + drawArc(path, + currentX, + currentY, + val[k + 5] + currentX, + val[k + 6] + currentY, + val[k + 0], + val[k + 1], + val[k + 2], + val[k + 3] != 0, + val[k + 4] != 0); + currentX += val[k + 5]; + currentY += val[k + 6]; + ctrlPointX = currentX; + ctrlPointY = currentY; + break; + case 'A': // Draws an elliptical arc + drawArc(path, + currentX, + currentY, + val[k + 5], + val[k + 6], + val[k + 0], + val[k + 1], + val[k + 2], + val[k + 3] != 0, + val[k + 4] != 0); + currentX = val[k + 5]; + currentY = val[k + 6]; + ctrlPointX = currentX; + ctrlPointY = currentY; + break; + } + previousCmd = cmd; + } + current[0] = currentX; + current[1] = currentY; + current[2] = ctrlPointX; + current[3] = ctrlPointY; + current[4] = currentSegmentStartX; + current[5] = currentSegmentStartY; + } + + private static void drawArc(Path p, + float x0, + float y0, + float x1, + float y1, + float a, + float b, + float theta, + boolean isMoreThanHalf, + boolean isPositiveArc) { + + /* Convert rotation angle from degrees to radians */ + double thetaD = Math.toRadians(theta); + /* Pre-compute rotation matrix entries */ + double cosTheta = Math.cos(thetaD); + double sinTheta = Math.sin(thetaD); + /* Transform (x0, y0) and (x1, y1) into unit space */ + /* using (inverse) rotation, followed by (inverse) scale */ + double x0p = (x0 * cosTheta + y0 * sinTheta) / a; + double y0p = (-x0 * sinTheta + y0 * cosTheta) / b; + double x1p = (x1 * cosTheta + y1 * sinTheta) / a; + double y1p = (-x1 * sinTheta + y1 * cosTheta) / b; + + /* Compute differences and averages */ + double dx = x0p - x1p; + double dy = y0p - y1p; + double xm = (x0p + x1p) / 2; + double ym = (y0p + y1p) / 2; + /* Solve for intersecting unit circles */ + double dsq = dx * dx + dy * dy; + if (dsq == 0.0) { + Log.w(LOGTAG, " Points are coincident"); + return; /* Points are coincident */ + } + double disc = 1.0 / dsq - 1.0 / 4.0; + if (disc < 0.0) { + Log.w(LOGTAG, "Points are too far apart " + dsq); + float adjust = (float) (Math.sqrt(dsq) / 1.99999); + drawArc(p, x0, y0, x1, y1, a * adjust, + b * adjust, theta, isMoreThanHalf, isPositiveArc); + return; /* Points are too far apart */ + } + double s = Math.sqrt(disc); + double sdx = s * dx; + double sdy = s * dy; + double cx; + double cy; + if (isMoreThanHalf == isPositiveArc) { + cx = xm - sdy; + cy = ym + sdx; + } else { + cx = xm + sdy; + cy = ym - sdx; + } + + double eta0 = Math.atan2((y0p - cy), (x0p - cx)); + + double eta1 = Math.atan2((y1p - cy), (x1p - cx)); + + double sweep = (eta1 - eta0); + if (isPositiveArc != (sweep >= 0)) { + if (sweep > 0) { + sweep -= 2 * Math.PI; + } else { + sweep += 2 * Math.PI; + } + } + + cx *= a; + cy *= b; + double tcx = cx; + cx = cx * cosTheta - cy * sinTheta; + cy = tcx * sinTheta + cy * cosTheta; + + arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); + } + + /** + * Converts an arc to cubic Bezier segments and records them in p. + * + * @param p The target for the cubic Bezier segments + * @param cx The x coordinate center of the ellipse + * @param cy The y coordinate center of the ellipse + * @param a The radius of the ellipse in the horizontal direction + * @param b The radius of the ellipse in the vertical direction + * @param e1x E(eta1) x coordinate of the starting point of the arc + * @param e1y E(eta2) y coordinate of the starting point of the arc + * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane + * @param start The start angle of the arc on the ellipse + * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse + */ + private static void arcToBezier(Path p, + double cx, + double cy, + double a, + double b, + double e1x, + double e1y, + double theta, + double start, + double sweep) { + // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html + // and http://www.spaceroots.org/documents/ellipse/node22.html + + // Maximum of 45 degrees per cubic Bezier segment + int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI)); + + double eta1 = start; + double cosTheta = Math.cos(theta); + double sinTheta = Math.sin(theta); + double cosEta1 = Math.cos(eta1); + double sinEta1 = Math.sin(eta1); + double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1); + double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1); + + double anglePerSegment = sweep / numSegments; + for (int i = 0; i < numSegments; i++) { + double eta2 = eta1 + anglePerSegment; + double sinEta2 = Math.sin(eta2); + double cosEta2 = Math.cos(eta2); + double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2); + double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2); + double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; + double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; + double tanDiff2 = Math.tan((eta2 - eta1) / 2); + double alpha = + Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3; + double q1x = e1x + alpha * ep1x; + double q1y = e1y + alpha * ep1y; + double q2x = e2x - alpha * ep2x; + double q2y = e2y - alpha * ep2y; + + p.cubicTo((float) q1x, + (float) q1y, + (float) q2x, + (float) q2y, + (float) e2x, + (float) e2y); + eta1 = eta2; + e1x = e2x; + e1y = e2y; + ep1x = ep2x; + ep1y = ep2y; + } + } + } +} diff --git a/src/main/java/android/util/Patterns.java b/src/main/java/android/util/Patterns.java new file mode 100644 index 0000000..2cc91b9 --- /dev/null +++ b/src/main/java/android/util/Patterns.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2007 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 android.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Commonly used regular expression patterns. + */ +public class Patterns { + /** + * Regular expression to match all IANA top-level domains. + * List accurate as of 2011/07/18. List taken from: + * http://data.iana.org/TLD/tlds-alpha-by-domain.txt + * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py + * + * @deprecated Due to the recent profileration of gTLDs, this API is + * expected to become out-of-date very quickly. Therefore it is now + * deprecated. + */ + @Deprecated + public static final String TOP_LEVEL_DOMAIN_STR = + "((aero|arpa|asia|a[cdefgilmnoqrstuwxz])" + + "|(biz|b[abdefghijmnorstvwyz])" + + "|(cat|com|coop|c[acdfghiklmnoruvxyz])" + + "|d[ejkmoz]" + + "|(edu|e[cegrstu])" + + "|f[ijkmor]" + + "|(gov|g[abdefghilmnpqrstuwy])" + + "|h[kmnrtu]" + + "|(info|int|i[delmnoqrst])" + + "|(jobs|j[emop])" + + "|k[eghimnprwyz]" + + "|l[abcikrstuvy]" + + "|(mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])" + + "|(name|net|n[acefgilopruz])" + + "|(org|om)" + + "|(pro|p[aefghklmnrstwy])" + + "|qa" + + "|r[eosuw]" + + "|s[abcdeghijklmnortuvyz]" + + "|(tel|travel|t[cdfghjklmnoprtvwz])" + + "|u[agksyz]" + + "|v[aceginu]" + + "|w[fs]" + + "|(\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)" + + "|y[et]" + + "|z[amw])"; + + /** + * Regular expression pattern to match all IANA top-level domains. + * @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}. + */ + @Deprecated + public static final Pattern TOP_LEVEL_DOMAIN = + Pattern.compile(TOP_LEVEL_DOMAIN_STR); + + /** + * Regular expression to match all IANA top-level domains for WEB_URL. + * List accurate as of 2011/07/18. List taken from: + * http://data.iana.org/TLD/tlds-alpha-by-domain.txt + * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py + * + * @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}. + */ + @Deprecated + public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL = + "(?:" + + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])" + + "|(?:biz|b[abdefghijmnorstvwyz])" + + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])" + + "|d[ejkmoz]" + + "|(?:edu|e[cegrstu])" + + "|f[ijkmor]" + + "|(?:gov|g[abdefghilmnpqrstuwy])" + + "|h[kmnrtu]" + + "|(?:info|int|i[delmnoqrst])" + + "|(?:jobs|j[emop])" + + "|k[eghimnprwyz]" + + "|l[abcikrstuvy]" + + "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])" + + "|(?:name|net|n[acefgilopruz])" + + "|(?:org|om)" + + "|(?:pro|p[aefghklmnrstwy])" + + "|qa" + + "|r[eosuw]" + + "|s[abcdeghijklmnortuvyz]" + + "|(?:tel|travel|t[cdfghjklmnoprtvwz])" + + "|u[agksyz]" + + "|v[aceginu]" + + "|w[fs]" + + "|(?:\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)" + + "|y[et]" + + "|z[amw]))"; + + /** + * Good characters for Internationalized Resource Identifiers (IRI). + * This comprises most common used Unicode characters allowed in IRI + * as detailed in RFC 3987. + * Specifically, those two byte Unicode characters are not included. + */ + public static final String GOOD_IRI_CHAR = + "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF"; + + public static final Pattern IP_ADDRESS + = Pattern.compile( + "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" + + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" + + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + + "|[1-9][0-9]|[0-9]))"); + + /** + * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets. + */ + private static final String IRI + = "[" + GOOD_IRI_CHAR + "]([" + GOOD_IRI_CHAR + "\\-]{0,61}[" + GOOD_IRI_CHAR + "]){0,1}"; + + private static final String GOOD_GTLD_CHAR = + "a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF"; + private static final String GTLD = "[" + GOOD_GTLD_CHAR + "]{2,63}"; + private static final String HOST_NAME = "(" + IRI + "\\.)+" + GTLD; + + public static final Pattern DOMAIN_NAME + = Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")"); + + /** + * Regular expression pattern to match most part of RFC 3987 + * Internationalized URLs, aka IRIs. Commonly used Unicode characters are + * added. + */ + public static final Pattern WEB_URL = Pattern.compile( + "((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" + + "(?:" + DOMAIN_NAME + ")" + + "(?:\\:\\d{1,5})?)" // plus option port number + + "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params + + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" + + "(?:\\b|$)"); // and finally, a word boundary or end of + // input. This is to stop foo.sure from + // matching as foo.su + + public static final Pattern EMAIL_ADDRESS + = Pattern.compile( + "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + + "\\@" + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + + "(" + + "\\." + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + + ")+" + ); + + /** + * This pattern is intended for searching for things that look like they + * might be phone numbers in arbitrary text, not for validating whether + * something is in fact a phone number. It will miss many things that + * are legitimate phone numbers. + * + *

    The pattern matches the following: + *

      + *
    • Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes + * may follow. + *
    • Optionally, sets of digits in parentheses, separated by spaces, dots, or dashes. + *
    • A string starting and ending with a digit, containing digits, spaces, dots, and/or dashes. + *
    + */ + public static final Pattern PHONE + = Pattern.compile( // sdd = space, dot, or dash + "(\\+[0-9]+[\\- \\.]*)?" // +* + + "(\\([0-9]+\\)[\\- \\.]*)?" // ()* + + "([0-9][0-9\\- \\.]+[0-9])"); // + + + /** + * Convenience method to take all of the non-null matching groups in a + * regex Matcher and return them as a concatenated string. + * + * @param matcher The Matcher object from which grouped text will + * be extracted + * + * @return A String comprising all of the non-null matched + * groups concatenated together + */ + public static final String concatGroups(Matcher matcher) { + StringBuilder b = new StringBuilder(); + final int numGroups = matcher.groupCount(); + + for (int i = 1; i <= numGroups; i++) { + String s = matcher.group(i); + + if (s != null) { + b.append(s); + } + } + + return b.toString(); + } + + /** + * Convenience method to return only the digits and plus signs + * in the matching string. + * + * @param matcher The Matcher object from which digits and plus will + * be extracted + * + * @return A String comprising all of the digits and plus in + * the match + */ + public static final String digitsAndPlusOnly(Matcher matcher) { + StringBuilder buffer = new StringBuilder(); + String matchingRegion = matcher.group(); + + for (int i = 0, size = matchingRegion.length(); i < size; i++) { + char character = matchingRegion.charAt(i); + + if (character == '+' || Character.isDigit(character)) { + buffer.append(character); + } + } + return buffer.toString(); + } + + /** + * Do not create this static utility class. + */ + private Patterns() {} +} diff --git a/src/main/java/android/util/PatternsTest.java b/src/main/java/android/util/PatternsTest.java new file mode 100644 index 0000000..ebdbb0e --- /dev/null +++ b/src/main/java/android/util/PatternsTest.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2010 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 android.util; + +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Patterns; + +import java.util.regex.Matcher; + +import junit.framework.TestCase; + +public class PatternsTest extends TestCase { + + @SmallTest + public void testTldPattern() throws Exception { + boolean t; + + t = Patterns.TOP_LEVEL_DOMAIN.matcher("com").matches(); + assertTrue("Missed valid TLD", t); + + // One of the new top level domain. + t = Patterns.TOP_LEVEL_DOMAIN.matcher("me").matches(); + assertTrue("Missed valid TLD", t); + + // One of the new top level test domain. + t = Patterns.TOP_LEVEL_DOMAIN.matcher("xn--0zwm56d").matches(); + assertTrue("Missed valid TLD", t); + + // One of the new top level internationalized domain. + t = Patterns.TOP_LEVEL_DOMAIN.matcher("\uD55C\uAD6D").matches(); + assertTrue("Missed valid TLD", t); + + t = Patterns.TOP_LEVEL_DOMAIN.matcher("mem").matches(); + assertFalse("Matched invalid TLD!", t); + + t = Patterns.TOP_LEVEL_DOMAIN.matcher("xn").matches(); + assertFalse("Matched invalid TLD!", t); + + t = Patterns.TOP_LEVEL_DOMAIN.matcher("xer").matches(); + assertFalse("Matched invalid TLD!", t); + } + + @SmallTest + public void testUrlPattern() throws Exception { + boolean t; + + t = Patterns.WEB_URL.matcher("http://www.google.com").matches(); + assertTrue("Valid URL", t); + + // Google in one of the new top level domain. + t = Patterns.WEB_URL.matcher("http://www.google.me").matches(); + assertTrue("Valid URL", t); + t = Patterns.WEB_URL.matcher("google.me").matches(); + assertTrue("Valid URL", t); + + // Test url in Chinese: http://xn--fsqu00a.xn--0zwm56d + t = Patterns.WEB_URL.matcher("http://xn--fsqu00a.xn--0zwm56d").matches(); + assertTrue("Valid URL", t); + t = Patterns.WEB_URL.matcher("xn--fsqu00a.xn--0zwm56d").matches(); + assertTrue("Valid URL", t); + + // Url for testing top level Arabic country code domain in Punycode: + // http://xn--4gbrim.xn----rmckbbajlc6dj7bxne2c.xn--wgbh1c/ar/default.aspx + t = Patterns.WEB_URL.matcher("http://xn--4gbrim.xn----rmckbbajlc6dj7bxne2c.xn--wgbh1c/ar/default.aspx").matches(); + assertTrue("Valid URL", t); + t = Patterns.WEB_URL.matcher("xn--4gbrim.xn----rmckbbajlc6dj7bxne2c.xn--wgbh1c/ar/default.aspx").matches(); + assertTrue("Valid URL", t); + + // Internationalized URL. + t = Patterns.WEB_URL.matcher("http://\uD604\uAE08\uC601\uC218\uC99D.kr").matches(); + assertTrue("Valid URL", t); + t = Patterns.WEB_URL.matcher("\uD604\uAE08\uC601\uC218\uC99D.kr").matches(); + assertTrue("Valid URL", t); + // URL with international TLD. + t = Patterns.WEB_URL.matcher("\uB3C4\uBA54\uC778.\uD55C\uAD6D").matches(); + assertTrue("Valid URL", t); + + t = Patterns.WEB_URL.matcher("http://brainstormtech.blogs.fortune.cnn.com/2010/03/11/" + + "top-five-moments-from-eric-schmidt\u2019s-talk-in-abu-dhabi/").matches(); + assertTrue("Valid URL", t); + + t = Patterns.WEB_URL.matcher("ftp://www.example.com").matches(); + assertFalse("Matched invalid protocol", t); + + t = Patterns.WEB_URL.matcher("http://www.example.com:8080").matches(); + assertTrue("Didn't match valid URL with port", t); + + t = Patterns.WEB_URL.matcher("http://www.example.com:8080/?foo=bar").matches(); + assertTrue("Didn't match valid URL with port and query args", t); + + t = Patterns.WEB_URL.matcher("http://www.example.com:8080/~user/?foo=bar").matches(); + assertTrue("Didn't match valid URL with ~", t); + } + + @SmallTest + public void testIpPattern() throws Exception { + boolean t; + + t = Patterns.IP_ADDRESS.matcher("172.29.86.3").matches(); + assertTrue("Valid IP", t); + + t = Patterns.IP_ADDRESS.matcher("1234.4321.9.9").matches(); + assertFalse("Invalid IP", t); + } + + @SmallTest + public void testDomainPattern() throws Exception { + boolean t; + + t = Patterns.DOMAIN_NAME.matcher("mail.example.com").matches(); + assertTrue("Valid domain", t); + + t = Patterns.DOMAIN_NAME.matcher("google.me").matches(); + assertTrue("Valid domain", t); + + // Internationalized domains. + t = Patterns.DOMAIN_NAME.matcher("\uD604\uAE08\uC601\uC218\uC99D.kr").matches(); + assertTrue("Valid domain", t); + + t = Patterns.DOMAIN_NAME.matcher("__+&42.xer").matches(); + assertFalse("Invalid domain", t); + + // Obsolete domain .yu + t = Patterns.DOMAIN_NAME.matcher("test.yu").matches(); + assertFalse("Obsolete country code top level domain", t); + + // Testing top level Arabic country code domain in Punycode: + t = Patterns.DOMAIN_NAME.matcher("xn--4gbrim.xn----rmckbbajlc6dj7bxne2c.xn--wgbh1c").matches(); + assertTrue("Valid domain", t); + } + + @SmallTest + public void testPhonePattern() throws Exception { + boolean t; + + t = Patterns.PHONE.matcher("(919) 555-1212").matches(); + assertTrue("Valid phone", t); + + t = Patterns.PHONE.matcher("2334 9323/54321").matches(); + assertFalse("Invalid phone", t); + + String[] tests = { + "Me: 16505551212 this\n", + "Me: 6505551212 this\n", + "Me: 5551212 this\n", + "Me: 2211 this\n", + "Me: 112 this\n", + + "Me: 1-650-555-1212 this\n", + "Me: (650) 555-1212 this\n", + "Me: +1 (650) 555-1212 this\n", + "Me: +1-650-555-1212 this\n", + "Me: 650-555-1212 this\n", + "Me: 555-1212 this\n", + + "Me: 1.650.555.1212 this\n", + "Me: (650) 555.1212 this\n", + "Me: +1 (650) 555.1212 this\n", + "Me: +1.650.555.1212 this\n", + "Me: 650.555.1212 this\n", + "Me: 555.1212 this\n", + + "Me: 1 650 555 1212 this\n", + "Me: (650) 555 1212 this\n", + "Me: +1 (650) 555 1212 this\n", + "Me: +1 650 555 1212 this\n", + "Me: 650 555 1212 this\n", + "Me: 555 1212 this\n", + }; + + for (String test : tests) { + Matcher m = Patterns.PHONE.matcher(test); + + assertTrue("Valid phone " + test, m.find()); + } + } +} diff --git a/src/main/java/android/util/Pools.java b/src/main/java/android/util/Pools.java new file mode 100644 index 0000000..70581be --- /dev/null +++ b/src/main/java/android/util/Pools.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2009 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 android.util; + +/** + * Helper class for crating pools of objects. An example use looks like this: + *
    + * public class MyPooledClass {
    + *
    + *     private static final SynchronizedPool sPool =
    + *             new SynchronizedPool(10);
    + *
    + *     public static MyPooledClass obtain() {
    + *         MyPooledClass instance = sPool.acquire();
    + *         return (instance != null) ? instance : new MyPooledClass();
    + *     }
    + *
    + *     public void recycle() {
    + *          // Clear state if needed.
    + *          sPool.release(this);
    + *     }
    + *
    + *     . . .
    + * }
    + * 
    + * + * @hide + */ +public final class Pools { + + /** + * Interface for managing a pool of objects. + * + * @param The pooled type. + */ + public static interface Pool { + + /** + * @return An instance from the pool if such, null otherwise. + */ + public T acquire(); + + /** + * Release an instance to the pool. + * + * @param instance The instance to release. + * @return Whether the instance was put in the pool. + * + * @throws IllegalStateException If the instance is already in the pool. + */ + public boolean release(T instance); + } + + private Pools() { + /* do nothing - hiding constructor */ + } + + /** + * Simple (non-synchronized) pool of objects. + * + * @param The pooled type. + */ + public static class SimplePool implements Pool { + private final Object[] mPool; + + private int mPoolSize; + + /** + * Creates a new instance. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + */ + public SimplePool(int maxPoolSize) { + if (maxPoolSize <= 0) { + throw new IllegalArgumentException("The max pool size must be > 0"); + } + mPool = new Object[maxPoolSize]; + } + + @Override + @SuppressWarnings("unchecked") + public T acquire() { + if (mPoolSize > 0) { + final int lastPooledIndex = mPoolSize - 1; + T instance = (T) mPool[lastPooledIndex]; + mPool[lastPooledIndex] = null; + mPoolSize--; + return instance; + } + return null; + } + + @Override + public boolean release(T instance) { + if (isInPool(instance)) { + throw new IllegalStateException("Already in the pool!"); + } + if (mPoolSize < mPool.length) { + mPool[mPoolSize] = instance; + mPoolSize++; + return true; + } + return false; + } + + private boolean isInPool(T instance) { + for (int i = 0; i < mPoolSize; i++) { + if (mPool[i] == instance) { + return true; + } + } + return false; + } + } + + /** + * Synchronized) pool of objects. + * + * @param The pooled type. + */ + public static class SynchronizedPool extends SimplePool { + private final Object mLock = new Object(); + + /** + * Creates a new instance. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + */ + public SynchronizedPool(int maxPoolSize) { + super(maxPoolSize); + } + + @Override + public T acquire() { + synchronized (mLock) { + return super.acquire(); + } + } + + @Override + public boolean release(T element) { + synchronized (mLock) { + return super.release(element); + } + } + } +} diff --git a/src/main/java/android/util/PrefixPrinter.java b/src/main/java/android/util/PrefixPrinter.java new file mode 100644 index 0000000..62f7da1 --- /dev/null +++ b/src/main/java/android/util/PrefixPrinter.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 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 android.util; + +/** + * PrefixPrinter is a Printer which prefixes all lines with a given + * prefix. + * + * @hide + */ +public class PrefixPrinter implements Printer { + private final Printer mPrinter; + private final String mPrefix; + + /** + * Creates a new PrefixPrinter. + * + *

    If prefix is null or empty, the provided printer is returned, rather + * than making a prefixing printer. + */ + public static Printer create(Printer printer, String prefix) { + if (prefix == null || prefix.equals("")) { + return printer; + } + return new PrefixPrinter(printer, prefix); + } + + private PrefixPrinter(Printer printer, String prefix) { + mPrinter = printer; + mPrefix = prefix; + } + + public void println(String str) { + mPrinter.println(mPrefix + str); + } +} diff --git a/src/main/java/android/util/PrintStreamPrinter.java b/src/main/java/android/util/PrintStreamPrinter.java new file mode 100644 index 0000000..1c11f15 --- /dev/null +++ b/src/main/java/android/util/PrintStreamPrinter.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2006 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 android.util; + +import java.io.PrintStream; + +/** + * Implementation of a {@link android.util.Printer} that sends its output + * to a {@link java.io.PrintStream}. + */ +public class PrintStreamPrinter implements Printer { + private final PrintStream mPS; + + /** + * Create a new Printer that sends to a PrintWriter object. + * + * @param pw The PrintWriter where you would like output to go. + */ + public PrintStreamPrinter(PrintStream pw) { + mPS = pw; + } + + public void println(String x) { + mPS.println(x); + } +} diff --git a/src/main/java/android/util/PrintWriterPrinter.java b/src/main/java/android/util/PrintWriterPrinter.java new file mode 100644 index 0000000..82c4d03 --- /dev/null +++ b/src/main/java/android/util/PrintWriterPrinter.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2006 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 android.util; + +import java.io.PrintWriter; + +/** + * Implementation of a {@link android.util.Printer} that sends its output + * to a {@link java.io.PrintWriter}. + */ +public class PrintWriterPrinter implements Printer { + private final PrintWriter mPW; + + /** + * Create a new Printer that sends to a PrintWriter object. + * + * @param pw The PrintWriter where you would like output to go. + */ + public PrintWriterPrinter(PrintWriter pw) { + mPW = pw; + } + + public void println(String x) { + mPW.println(x); + } +} diff --git a/src/main/java/android/util/Printer.java b/src/main/java/android/util/Printer.java new file mode 100644 index 0000000..595cf70 --- /dev/null +++ b/src/main/java/android/util/Printer.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2006 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 android.util; + +/** + * Simple interface for printing text, allowing redirection to various + * targets. Standard implementations are {@link android.util.LogPrinter}, + * {@link android.util.StringBuilderPrinter}, and + * {@link android.util.PrintWriterPrinter}. + */ +public interface Printer { + /** + * Write a line of text to the output. There is no need to terminate + * the given string with a newline. + */ + void println(String x); +} diff --git a/src/main/java/android/util/Property.java b/src/main/java/android/util/Property.java new file mode 100644 index 0000000..146db80 --- /dev/null +++ b/src/main/java/android/util/Property.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2011 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 android.util; + + +/** + * A property is an abstraction that can be used to represent a mutable value that is held + * in a host object. The Property's {@link #set(Object, Object)} or {@link #get(Object)} + * methods can be implemented in terms of the private fields of the host object, or via "setter" and + * "getter" methods or by some other mechanism, as appropriate. + * + * @param The class on which the property is declared. + * @param The type that this property represents. + */ +public abstract class Property { + + private final String mName; + private final Class mType; + + /** + * This factory method creates and returns a Property given the class and + * name parameters, where the "name" parameter represents either: + *

      + *
    • a public getName() method on the class which takes no arguments, plus an + * optional public setName() method which takes a value of the same type + * returned by getName() + *
    • a public isName() method on the class which takes no arguments, plus an + * optional public setName() method which takes a value of the same type + * returned by isName() + *
    • a public name field on the class + *
    + * + *

    If either of the get/is method alternatives is found on the class, but an appropriate + * setName() method is not found, the Property will be + * {@link #isReadOnly() readOnly}. Calling the {@link #set(Object, Object)} method on such + * a property is allowed, but will have no effect.

    + * + *

    If neither the methods nor the field are found on the class a + * {@link NoSuchPropertyException} exception will be thrown.

    + */ + public static Property of(Class hostType, Class valueType, String name) { + return new ReflectiveProperty(hostType, valueType, name); + } + + /** + * A constructor that takes an identifying name and {@link #getType() type} for the property. + */ + public Property(Class type, String name) { + mName = name; + mType = type; + } + + /** + * Returns true if the {@link #set(Object, Object)} method does not set the value on the target + * object (in which case the {@link #set(Object, Object) set()} method should throw a {@link + * NoSuchPropertyException} exception). This may happen if the Property wraps functionality that + * allows querying the underlying value but not setting it. For example, the {@link #of(Class, + * Class, String)} factory method may return a Property with name "foo" for an object that has + * only a getFoo() or isFoo() method, but no matching + * setFoo() method. + */ + public boolean isReadOnly() { + return false; + } + + /** + * Sets the value on object which this property represents. If the method is unable + * to set the value on the target object it will throw an {@link UnsupportedOperationException} + * exception. + */ + public void set(T object, V value) { + throw new UnsupportedOperationException("Property " + getName() +" is read-only"); + } + + /** + * Returns the current value that this property represents on the given object. + */ + public abstract V get(T object); + + /** + * Returns the name for this property. + */ + public String getName() { + return mName; + } + + /** + * Returns the type for this property. + */ + public Class getType() { + return mType; + } +} diff --git a/src/main/java/android/util/Range.java b/src/main/java/android/util/Range.java new file mode 100644 index 0000000..211d01a --- /dev/null +++ b/src/main/java/android/util/Range.java @@ -0,0 +1,358 @@ +/* + * 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 android.util; + +import static com.android.internal.util.Preconditions.*; + +import android.hardware.camera2.utils.HashCodeHelpers; + +/** + * Immutable class for describing the range of two numeric values. + *

    + * A range (or "interval") defines the inclusive boundaries around a contiguous span of + * values of some {@link Comparable} type; for example, + * "integers from 1 to 100 inclusive." + *

    + *

    + * All ranges are bounded, and the left side of the range is always {@code >=} + * the right side of the range. + *

    + * + *

    Although the implementation itself is immutable, there is no restriction that objects + * stored must also be immutable. If mutable objects are stored here, then the range + * effectively becomes mutable.

    + */ +public final class Range> { + /** + * Create a new immutable range. + * + *

    + * The endpoints are {@code [lower, upper]}; that + * is the range is bounded. {@code lower} must be {@link Comparable#compareTo lesser or equal} + * to {@code upper}. + *

    + * + * @param lower The lower endpoint (inclusive) + * @param upper The upper endpoint (inclusive) + * + * @throws NullPointerException if {@code lower} or {@code upper} is {@code null} + */ + public Range(final T lower, final T upper) { + mLower = checkNotNull(lower, "lower must not be null"); + mUpper = checkNotNull(upper, "upper must not be null"); + + if (lower.compareTo(upper) > 0) { + throw new IllegalArgumentException("lower must be less than or equal to upper"); + } + } + + /** + * Create a new immutable range, with the argument types inferred. + * + *

    + * The endpoints are {@code [lower, upper]}; that + * is the range is bounded. {@code lower} must be {@link Comparable#compareTo lesser or equal} + * to {@code upper}. + *

    + * + * @param lower The lower endpoint (inclusive) + * @param upper The upper endpoint (inclusive) + * + * @throws NullPointerException if {@code lower} or {@code upper} is {@code null} + */ + public static > Range create(final T lower, final T upper) { + return new Range(lower, upper); + } + + /** + * Get the lower endpoint. + * + * @return a non-{@code null} {@code T} reference + */ + public T getLower() { + return mLower; + } + + /** + * Get the upper endpoint. + * + * @return a non-{@code null} {@code T} reference + */ + public T getUpper() { + return mUpper; + } + + /** + * Checks if the {@code value} is within the bounds of this range. + * + *

    A value is considered to be within this range if it's {@code >=} + * the lower endpoint and {@code <=} the upper endpoint (using the {@link Comparable} + * interface.)

    + * + * @param value a non-{@code null} {@code T} reference + * @return {@code true} if the value is within this inclusive range, {@code false} otherwise + * + * @throws NullPointerException if {@code value} was {@code null} + */ + public boolean contains(T value) { + checkNotNull(value, "value must not be null"); + + boolean gteLower = value.compareTo(mLower) >= 0; + boolean lteUpper = value.compareTo(mUpper) <= 0; + + return gteLower && lteUpper; + } + + /** + * Checks if another {@code range} is within the bounds of this range. + * + *

    A range is considered to be within this range if both of its endpoints + * are within this range.

    + * + * @param range a non-{@code null} {@code T} reference + * @return {@code true} if the range is within this inclusive range, {@code false} otherwise + * + * @throws NullPointerException if {@code range} was {@code null} + */ + public boolean contains(Range range) { + checkNotNull(range, "value must not be null"); + + boolean gteLower = range.mLower.compareTo(mLower) >= 0; + boolean lteUpper = range.mUpper.compareTo(mUpper) <= 0; + + return gteLower && lteUpper; + } + + /** + * Compare two ranges for equality. + * + *

    A range is considered equal if and only if both the lower and upper endpoints + * are also equal.

    + * + * @return {@code true} if the ranges are equal, {@code false} otherwise + */ + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } else if (this == obj) { + return true; + } else if (obj instanceof Range) { + @SuppressWarnings("rawtypes") + Range other = (Range) obj; + return mLower.equals(other.mLower) && mUpper.equals(other.mUpper); + } + return false; + } + + /** + * Clamps {@code value} to this range. + * + *

    If the value is within this range, it is returned. Otherwise, if it + * is {@code <} than the lower endpoint, the lower endpoint is returned, + * else the upper endpoint is returned. Comparisons are performed using the + * {@link Comparable} interface.

    + * + * @param value a non-{@code null} {@code T} reference + * @return {@code value} clamped to this range. + */ + public T clamp(T value) { + checkNotNull(value, "value must not be null"); + + if (value.compareTo(mLower) < 0) { + return mLower; + } else if (value.compareTo(mUpper) > 0) { + return mUpper; + } else { + return value; + } + } + + /** + * Returns the intersection of this range and another {@code range}. + *

    + * E.g. if a {@code <} b {@code <} c {@code <} d, the + * intersection of [a, c] and [b, d] ranges is [b, c]. + * As the endpoints are object references, there is no guarantee + * which specific endpoint reference is used from the input ranges:

    + *

    + * E.g. if a {@code ==} a' {@code <} b {@code <} c, the + * intersection of [a, b] and [a', c] ranges could be either + * [a, b] or ['a, b], where [a, b] could be either the exact + * input range, or a newly created range with the same endpoints.

    + * + * @param range a non-{@code null} {@code Range} reference + * @return the intersection of this range and the other range. + * + * @throws NullPointerException if {@code range} was {@code null} + * @throws IllegalArgumentException if the ranges are disjoint. + */ + public Range intersect(Range range) { + checkNotNull(range, "range must not be null"); + + int cmpLower = range.mLower.compareTo(mLower); + int cmpUpper = range.mUpper.compareTo(mUpper); + + if (cmpLower <= 0 && cmpUpper >= 0) { + // range includes this + return this; + } else if (cmpLower >= 0 && cmpUpper <= 0) { + // this inludes range + return range; + } else { + return Range.create( + cmpLower <= 0 ? mLower : range.mLower, + cmpUpper >= 0 ? mUpper : range.mUpper); + } + } + + /** + * Returns the intersection of this range and the inclusive range + * specified by {@code [lower, upper]}. + *

    + * See {@link #intersect(Range)} for more details.

    + * + * @param lower a non-{@code null} {@code T} reference + * @param upper a non-{@code null} {@code T} reference + * @return the intersection of this range and the other range + * + * @throws NullPointerException if {@code lower} or {@code upper} was {@code null} + * @throws IllegalArgumentException if the ranges are disjoint. + */ + public Range intersect(T lower, T upper) { + checkNotNull(lower, "lower must not be null"); + checkNotNull(upper, "upper must not be null"); + + int cmpLower = lower.compareTo(mLower); + int cmpUpper = upper.compareTo(mUpper); + + if (cmpLower <= 0 && cmpUpper >= 0) { + // [lower, upper] includes this + return this; + } else { + return Range.create( + cmpLower <= 0 ? mLower : lower, + cmpUpper >= 0 ? mUpper : upper); + } + } + + /** + * Returns the smallest range that includes this range and + * another {@code range}. + *

    + * E.g. if a {@code <} b {@code <} c {@code <} d, the + * extension of [a, c] and [b, d] ranges is [a, d]. + * As the endpoints are object references, there is no guarantee + * which specific endpoint reference is used from the input ranges:

    + *

    + * E.g. if a {@code ==} a' {@code <} b {@code <} c, the + * extension of [a, b] and [a', c] ranges could be either + * [a, c] or ['a, c], where ['a, c] could be either the exact + * input range, or a newly created range with the same endpoints.

    + * + * @param range a non-{@code null} {@code Range} reference + * @return the extension of this range and the other range. + * + * @throws NullPointerException if {@code range} was {@code null} + */ + public Range extend(Range range) { + checkNotNull(range, "range must not be null"); + + int cmpLower = range.mLower.compareTo(mLower); + int cmpUpper = range.mUpper.compareTo(mUpper); + + if (cmpLower <= 0 && cmpUpper >= 0) { + // other includes this + return range; + } else if (cmpLower >= 0 && cmpUpper <= 0) { + // this inludes other + return this; + } else { + return Range.create( + cmpLower >= 0 ? mLower : range.mLower, + cmpUpper <= 0 ? mUpper : range.mUpper); + } + } + + /** + * Returns the smallest range that includes this range and + * the inclusive range specified by {@code [lower, upper]}. + *

    + * See {@link #extend(Range)} for more details.

    + * + * @param lower a non-{@code null} {@code T} reference + * @param upper a non-{@code null} {@code T} reference + * @return the extension of this range and the other range. + * + * @throws NullPointerException if {@code lower} or {@code + * upper} was {@code null} + */ + public Range extend(T lower, T upper) { + checkNotNull(lower, "lower must not be null"); + checkNotNull(upper, "upper must not be null"); + + int cmpLower = lower.compareTo(mLower); + int cmpUpper = upper.compareTo(mUpper); + + if (cmpLower >= 0 && cmpUpper <= 0) { + // this inludes other + return this; + } else { + return Range.create( + cmpLower >= 0 ? mLower : lower, + cmpUpper <= 0 ? mUpper : upper); + } + } + + /** + * Returns the smallest range that includes this range and + * the {@code value}. + *

    + * See {@link #extend(Range)} for more details, as this method is + * equivalent to {@code extend(Range.create(value, value))}.

    + * + * @param value a non-{@code null} {@code T} reference + * @return the extension of this range and the value. + * + * @throws NullPointerException if {@code value} was {@code null} + */ + public Range extend(T value) { + checkNotNull(value, "value must not be null"); + return extend(value, value); + } + + /** + * Return the range as a string representation {@code "[lower, upper]"}. + * + * @return string representation of the range + */ + @Override + public String toString() { + return String.format("[%s, %s]", mLower, mUpper); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return HashCodeHelpers.hashCode(mLower, mUpper); + } + + private final T mLower; + private final T mUpper; +}; diff --git a/src/main/java/android/util/Rational.java b/src/main/java/android/util/Rational.java new file mode 100644 index 0000000..80d26d9 --- /dev/null +++ b/src/main/java/android/util/Rational.java @@ -0,0 +1,602 @@ +/* + * 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 android.util; + +import static com.android.internal.util.Preconditions.*; + +import java.io.IOException; +import java.io.InvalidObjectException; + +/** + *

    An immutable data type representation a rational number.

    + * + *

    Contains a pair of {@code int}s representing the numerator and denominator of a + * Rational number.

    + */ +public final class Rational extends Number implements Comparable { + /** + * Constant for the Not-a-Number (NaN) value of the {@code Rational} type. + * + *

    A {@code NaN} value is considered to be equal to itself (that is {@code NaN.equals(NaN)} + * will return {@code true}; it is always greater than any non-{@code NaN} value (that is + * {@code NaN.compareTo(notNaN)} will return a number greater than {@code 0}).

    + * + *

    Equivalent to constructing a new rational with both the numerator and denominator + * equal to {@code 0}.

    + */ + public static final Rational NaN = new Rational(0, 0); + + /** + * Constant for the positive infinity value of the {@code Rational} type. + * + *

    Equivalent to constructing a new rational with a positive numerator and a denominator + * equal to {@code 0}.

    + */ + public static final Rational POSITIVE_INFINITY = new Rational(1, 0); + + /** + * Constant for the negative infinity value of the {@code Rational} type. + * + *

    Equivalent to constructing a new rational with a negative numerator and a denominator + * equal to {@code 0}.

    + */ + public static final Rational NEGATIVE_INFINITY = new Rational(-1, 0); + + /** + * Constant for the zero value of the {@code Rational} type. + * + *

    Equivalent to constructing a new rational with a numerator equal to {@code 0} and + * any non-zero denominator.

    + */ + public static final Rational ZERO = new Rational(0, 1); + + /** + * Unique version number per class to be compliant with {@link java.io.Serializable}. + * + *

    Increment each time the fields change in any way.

    + */ + private static final long serialVersionUID = 1L; + + /* + * Do not change the order of these fields or add new instance fields to maintain the + * Serializable compatibility across API revisions. + */ + private final int mNumerator; + private final int mDenominator; + + /** + *

    Create a {@code Rational} with a given numerator and denominator.

    + * + *

    The signs of the numerator and the denominator may be flipped such that the denominator + * is always positive. Both the numerator and denominator will be converted to their reduced + * forms (see {@link #equals} for more details).

    + * + *

    For example, + *

      + *
    • a rational of {@code 2/4} will be reduced to {@code 1/2}. + *
    • a rational of {@code 1/-1} will be flipped to {@code -1/1} + *
    • a rational of {@code 5/0} will be reduced to {@code 1/0} + *
    • a rational of {@code 0/5} will be reduced to {@code 0/1} + *
    + *

    + * + * @param numerator the numerator of the rational + * @param denominator the denominator of the rational + * + * @see #equals + */ + public Rational(int numerator, int denominator) { + + if (denominator < 0) { + numerator = -numerator; + denominator = -denominator; + } + + // Convert to reduced form + if (denominator == 0 && numerator > 0) { + mNumerator = 1; // +Inf + mDenominator = 0; + } else if (denominator == 0 && numerator < 0) { + mNumerator = -1; // -Inf + mDenominator = 0; + } else if (denominator == 0 && numerator == 0) { + mNumerator = 0; // NaN + mDenominator = 0; + } else if (numerator == 0) { + mNumerator = 0; + mDenominator = 1; + } else { + int gcd = gcd(numerator, denominator); + + mNumerator = numerator / gcd; + mDenominator = denominator / gcd; + } + } + + /** + * Gets the numerator of the rational. + * + *

    The numerator will always return {@code 1} if this rational represents + * infinity (that is, the denominator is {@code 0}).

    + */ + public int getNumerator() { + return mNumerator; + } + + /** + * Gets the denominator of the rational + * + *

    The denominator may return {@code 0}, in which case the rational may represent + * positive infinity (if the numerator was positive), negative infinity (if the numerator + * was negative), or {@code NaN} (if the numerator was {@code 0}).

    + * + *

    The denominator will always return {@code 1} if the numerator is {@code 0}. + */ + public int getDenominator() { + return mDenominator; + } + + /** + * Indicates whether this rational is a Not-a-Number (NaN) value. + * + *

    A {@code NaN} value occurs when both the numerator and the denominator are {@code 0}.

    + * + * @return {@code true} if this rational is a Not-a-Number (NaN) value; + * {@code false} if this is a (potentially infinite) number value + */ + public boolean isNaN() { + return mDenominator == 0 && mNumerator == 0; + } + + /** + * Indicates whether this rational represents an infinite value. + * + *

    An infinite value occurs when the denominator is {@code 0} (but the numerator is not).

    + * + * @return {@code true} if this rational is a (positive or negative) infinite value; + * {@code false} if this is a finite number value (or {@code NaN}) + */ + public boolean isInfinite() { + return mNumerator != 0 && mDenominator == 0; + } + + /** + * Indicates whether this rational represents a finite value. + * + *

    A finite value occurs when the denominator is not {@code 0}; in other words + * the rational is neither infinity or {@code NaN}.

    + * + * @return {@code true} if this rational is a (positive or negative) infinite value; + * {@code false} if this is a finite number value (or {@code NaN}) + */ + public boolean isFinite() { + return mDenominator != 0; + } + + /** + * Indicates whether this rational represents a zero value. + * + *

    A zero value is a {@link #isFinite finite} rational with a numerator of {@code 0}.

    + * + * @return {@code true} if this rational is finite zero value; + * {@code false} otherwise + */ + public boolean isZero() { + return isFinite() && mNumerator == 0; + } + + private boolean isPosInf() { + return mDenominator == 0 && mNumerator > 0; + } + + private boolean isNegInf() { + return mDenominator == 0 && mNumerator < 0; + } + + /** + *

    Compare this Rational to another object and see if they are equal.

    + * + *

    A Rational object can only be equal to another Rational object (comparing against any + * other type will return {@code false}).

    + * + *

    A Rational object is considered equal to another Rational object if and only if one of + * the following holds:

    + *
    • Both are {@code NaN}
    • + *
    • Both are infinities of the same sign
    • + *
    • Both have the same numerator and denominator in their reduced form
    • + *
    + * + *

    A reduced form of a Rational is calculated by dividing both the numerator and the + * denominator by their greatest common divisor.

    + * + *
    {@code
    +     * (new Rational(1, 2)).equals(new Rational(1, 2)) == true   // trivially true
    +     * (new Rational(2, 3)).equals(new Rational(1, 2)) == false  // trivially false
    +     * (new Rational(1, 2)).equals(new Rational(2, 4)) == true   // true after reduction
    +     * (new Rational(0, 0)).equals(new Rational(0, 0)) == true   // NaN.equals(NaN)
    +     * (new Rational(1, 0)).equals(new Rational(5, 0)) == true   // both are +infinity
    +     * (new Rational(1, 0)).equals(new Rational(-1, 0)) == false // +infinity != -infinity
    +     * }
    + * + * @param obj a reference to another object + * + * @return A boolean that determines whether or not the two Rational objects are equal. + */ + @Override + public boolean equals(Object obj) { + return obj instanceof Rational && equals((Rational) obj); + } + + private boolean equals(Rational other) { + return (mNumerator == other.mNumerator && mDenominator == other.mDenominator); + } + + /** + * Return a string representation of this rational, e.g. {@code "1/2"}. + * + *

    The following rules of conversion apply: + *

      + *
    • {@code NaN} values will return {@code "NaN"} + *
    • Positive infinity values will return {@code "Infinity"} + *
    • Negative infinity values will return {@code "-Infinity"} + *
    • All other values will return {@code "numerator/denominator"} where {@code numerator} + * and {@code denominator} are substituted with the appropriate numerator and denominator + * values. + *

    + */ + @Override + public String toString() { + if (isNaN()) { + return "NaN"; + } else if (isPosInf()) { + return "Infinity"; + } else if (isNegInf()) { + return "-Infinity"; + } else { + return mNumerator + "/" + mDenominator; + } + } + + /** + *

    Convert to a floating point representation.

    + * + * @return The floating point representation of this rational number. + * @hide + */ + public float toFloat() { + // TODO: remove this duplicate function (used in CTS and the shim) + return floatValue(); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + // Bias the hash code for the first (2^16) values for both numerator and denominator + int numeratorFlipped = mNumerator << 16 | mNumerator >>> 16; + + return mDenominator ^ numeratorFlipped; + } + + /** + * Calculates the greatest common divisor using Euclid's algorithm. + * + *

    Visible for testing only.

    + * + * @param numerator the numerator in a fraction + * @param denominator the denominator in a fraction + * + * @return An int value representing the gcd. Always positive. + * @hide + */ + public static int gcd(int numerator, int denominator) { + /* + * Non-recursive implementation of Euclid's algorithm: + * + * gcd(a, 0) := a + * gcd(a, b) := gcd(b, a mod b) + * + */ + int a = numerator; + int b = denominator; + + while (b != 0) { + int oldB = b; + + b = a % b; + a = oldB; + } + + return Math.abs(a); + } + + /** + * Returns the value of the specified number as a {@code double}. + * + *

    The {@code double} is calculated by converting both the numerator and denominator + * to a {@code double}; then returning the result of dividing the numerator by the + * denominator.

    + * + * @return the divided value of the numerator and denominator as a {@code double}. + */ + @Override + public double doubleValue() { + double num = mNumerator; + double den = mDenominator; + + return num / den; + } + + /** + * Returns the value of the specified number as a {@code float}. + * + *

    The {@code float} is calculated by converting both the numerator and denominator + * to a {@code float}; then returning the result of dividing the numerator by the + * denominator.

    + * + * @return the divided value of the numerator and denominator as a {@code float}. + */ + @Override + public float floatValue() { + float num = mNumerator; + float den = mDenominator; + + return num / den; + } + + /** + * Returns the value of the specified number as a {@code int}. + * + *

    {@link #isInfinite Finite} rationals are converted to an {@code int} value + * by dividing the numerator by the denominator; conversion for non-finite values happens + * identically to casting a floating point value to an {@code int}, in particular: + * + *

    + *

      + *
    • Positive infinity saturates to the largest maximum integer + * {@link Integer#MAX_VALUE}
    • + *
    • Negative infinity saturates to the smallest maximum integer + * {@link Integer#MIN_VALUE}
    • + *
    • Not-A-Number (NaN) returns {@code 0}.
    • + *
    + *

    + * + * @return the divided value of the numerator and denominator as a {@code int}. + */ + @Override + public int intValue() { + // Mimic float to int conversion rules from JLS 5.1.3 + + if (isPosInf()) { + return Integer.MAX_VALUE; + } else if (isNegInf()) { + return Integer.MIN_VALUE; + } else if (isNaN()) { + return 0; + } else { // finite + return mNumerator / mDenominator; + } + } + + /** + * Returns the value of the specified number as a {@code long}. + * + *

    {@link #isInfinite Finite} rationals are converted to an {@code long} value + * by dividing the numerator by the denominator; conversion for non-finite values happens + * identically to casting a floating point value to a {@code long}, in particular: + * + *

    + *

      + *
    • Positive infinity saturates to the largest maximum long + * {@link Long#MAX_VALUE}
    • + *
    • Negative infinity saturates to the smallest maximum long + * {@link Long#MIN_VALUE}
    • + *
    • Not-A-Number (NaN) returns {@code 0}.
    • + *
    + *

    + * + * @return the divided value of the numerator and denominator as a {@code long}. + */ + @Override + public long longValue() { + // Mimic float to long conversion rules from JLS 5.1.3 + + if (isPosInf()) { + return Long.MAX_VALUE; + } else if (isNegInf()) { + return Long.MIN_VALUE; + } else if (isNaN()) { + return 0; + } else { // finite + return mNumerator / mDenominator; + } + } + + /** + * Returns the value of the specified number as a {@code short}. + * + *

    {@link #isInfinite Finite} rationals are converted to a {@code short} value + * identically to {@link #intValue}; the {@code int} result is then truncated to a + * {@code short} before returning the value.

    + * + * @return the divided value of the numerator and denominator as a {@code short}. + */ + @Override + public short shortValue() { + return (short) intValue(); + } + + /** + * Compare this rational to the specified rational to determine their natural order. + * + *

    {@link #NaN} is considered to be equal to itself and greater than all other + * {@code Rational} values. Otherwise, if the objects are not {@link #equals equal}, then + * the following rules apply:

    + * + *
      + *
    • Positive infinity is greater than any other finite number (or negative infinity) + *
    • Negative infinity is less than any other finite number (or positive infinity) + *
    • The finite number represented by this rational is checked numerically + * against the other finite number by converting both rationals to a common denominator multiple + * and comparing their numerators. + *
    + * + * @param another the rational to be compared + * + * @return a negative integer, zero, or a positive integer as this object is less than, + * equal to, or greater than the specified rational. + * + * @throws NullPointerException if {@code another} was {@code null} + */ + @Override + public int compareTo(Rational another) { + checkNotNull(another, "another must not be null"); + + if (equals(another)) { + return 0; + } else if (isNaN()) { // NaN is greater than the other non-NaN value + return 1; + } else if (another.isNaN()) { // the other NaN is greater than this non-NaN value + return -1; + } else if (isPosInf() || another.isNegInf()) { + return 1; // positive infinity is greater than any non-NaN/non-posInf value + } else if (isNegInf() || another.isPosInf()) { + return -1; // negative infinity is less than any non-NaN/non-negInf value + } + + // else both this and another are finite numbers + + // make the denominators the same, then compare numerators + long thisNumerator = ((long)mNumerator) * another.mDenominator; // long to avoid overflow + long otherNumerator = ((long)another.mNumerator) * mDenominator; // long to avoid overflow + + // avoid underflow from subtraction by doing comparisons + if (thisNumerator < otherNumerator) { + return -1; + } else if (thisNumerator > otherNumerator) { + return 1; + } else { + // This should be covered by #equals, but have this code path just in case + return 0; + } + } + + /* + * Serializable implementation. + * + * The following methods are omitted: + * >> writeObject - the default is sufficient (field by field serialization) + * >> readObjectNoData - the default is sufficient (0s for both fields is a NaN) + */ + + /** + * writeObject with default serialized form - guards against + * deserializing non-reduced forms of the rational. + * + * @throws InvalidObjectException if the invariants were violated + */ + private void readObject(java.io.ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + + /* + * Guard against trying to deserialize illegal values (in this case, ones + * that don't have a standard reduced form). + * + * - Non-finite values must be one of [0, 1], [0, 0], [0, 1], [0, -1] + * - Finite values must always have their greatest common divisor as 1 + */ + + if (mNumerator == 0) { // either zero or NaN + if (mDenominator == 1 || mDenominator == 0) { + return; + } + throw new InvalidObjectException( + "Rational must be deserialized from a reduced form for zero values"); + } else if (mDenominator == 0) { // either positive or negative infinity + if (mNumerator == 1 || mNumerator == -1) { + return; + } + throw new InvalidObjectException( + "Rational must be deserialized from a reduced form for infinity values"); + } else { // finite value + if (gcd(mNumerator, mDenominator) > 1) { + throw new InvalidObjectException( + "Rational must be deserialized from a reduced form for finite values"); + } + } + } + + private static NumberFormatException invalidRational(String s) { + throw new NumberFormatException("Invalid Rational: \"" + s + "\""); + } + + /** + * Parses the specified string as a rational value. + *

    The ASCII characters {@code \}{@code u003a} (':') and + * {@code \}{@code u002f} ('/') are recognized as separators between + * the numerator and denumerator.

    + *

    + * For any {@code Rational r}: {@code Rational.parseRational(r.toString()).equals(r)}. + * However, the method also handles rational numbers expressed in the + * following forms:

    + *

    + * "num{@code /}den" or + * "num{@code :}den" {@code => new Rational(num, den);}, + * where num and den are string integers potentially + * containing a sign, such as "-10", "+7" or "5".

    + * + *
    {@code
    +     * Rational.parseRational("3:+6").equals(new Rational(1, 2)) == true
    +     * Rational.parseRational("-3/-6").equals(new Rational(1, 2)) == true
    +     * Rational.parseRational("4.56") => throws NumberFormatException
    +     * }
    + * + * @param string the string representation of a rational value. + * @return the rational value represented by {@code string}. + * + * @throws NumberFormatException if {@code string} cannot be parsed + * as a rational value. + * @throws NullPointerException if {@code string} was {@code null} + */ + public static Rational parseRational(String string) + throws NumberFormatException { + checkNotNull(string, "string must not be null"); + + if (string.equals("NaN")) { + return NaN; + } else if (string.equals("Infinity")) { + return POSITIVE_INFINITY; + } else if (string.equals("-Infinity")) { + return NEGATIVE_INFINITY; + } + + int sep_ix = string.indexOf(':'); + if (sep_ix < 0) { + sep_ix = string.indexOf('/'); + } + if (sep_ix < 0) { + throw invalidRational(string); + } + try { + return new Rational(Integer.parseInt(string.substring(0, sep_ix)), + Integer.parseInt(string.substring(sep_ix + 1))); + } catch (NumberFormatException e) { + throw invalidRational(string); + } + } +} diff --git a/src/main/java/android/util/ReflectiveProperty.java b/src/main/java/android/util/ReflectiveProperty.java new file mode 100644 index 0000000..6832240 --- /dev/null +++ b/src/main/java/android/util/ReflectiveProperty.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2011 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 android.util; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Internal class to automatically generate a Property for a given class/name pair, given the + * specification of {@link Property#of(java.lang.Class, java.lang.Class, java.lang.String)} + */ +class ReflectiveProperty extends Property { + + private static final String PREFIX_GET = "get"; + private static final String PREFIX_IS = "is"; + private static final String PREFIX_SET = "set"; + private Method mSetter; + private Method mGetter; + private Field mField; + + /** + * For given property name 'name', look for getName/isName method or 'name' field. + * Also look for setName method (optional - could be readonly). Failing method getters and + * field results in throwing NoSuchPropertyException. + * + * @param propertyHolder The class on which the methods or field are found + * @param name The name of the property, where this name is capitalized and appended to + * "get" and "is to search for the appropriate methods. If the get/is methods are not found, + * the constructor will search for a field with that exact name. + */ + public ReflectiveProperty(Class propertyHolder, Class valueType, String name) { + // TODO: cache reflection info for each new class/name pair + super(valueType, name); + char firstLetter = Character.toUpperCase(name.charAt(0)); + String theRest = name.substring(1); + String capitalizedName = firstLetter + theRest; + String getterName = PREFIX_GET + capitalizedName; + try { + mGetter = propertyHolder.getMethod(getterName, (Class[])null); + } catch (NoSuchMethodException e) { + // getName() not available - try isName() instead + getterName = PREFIX_IS + capitalizedName; + try { + mGetter = propertyHolder.getMethod(getterName, (Class[])null); + } catch (NoSuchMethodException e1) { + // Try public field instead + try { + mField = propertyHolder.getField(name); + Class fieldType = mField.getType(); + if (!typesMatch(valueType, fieldType)) { + throw new NoSuchPropertyException("Underlying type (" + fieldType + ") " + + "does not match Property type (" + valueType + ")"); + } + return; + } catch (NoSuchFieldException e2) { + // no way to access property - throw appropriate exception + throw new NoSuchPropertyException("No accessor method or field found for" + + " property with name " + name); + } + } + } + Class getterType = mGetter.getReturnType(); + // Check to make sure our getter type matches our valueType + if (!typesMatch(valueType, getterType)) { + throw new NoSuchPropertyException("Underlying type (" + getterType + ") " + + "does not match Property type (" + valueType + ")"); + } + String setterName = PREFIX_SET + capitalizedName; + try { + mSetter = propertyHolder.getMethod(setterName, getterType); + } catch (NoSuchMethodException ignored) { + // Okay to not have a setter - just a readonly property + } + } + + /** + * Utility method to check whether the type of the underlying field/method on the target + * object matches the type of the Property. The extra checks for primitive types are because + * generics will force the Property type to be a class, whereas the type of the underlying + * method/field will probably be a primitive type instead. Accept float as matching Float, + * etc. + */ + private boolean typesMatch(Class valueType, Class getterType) { + if (getterType != valueType) { + if (getterType.isPrimitive()) { + return (getterType == float.class && valueType == Float.class) || + (getterType == int.class && valueType == Integer.class) || + (getterType == boolean.class && valueType == Boolean.class) || + (getterType == long.class && valueType == Long.class) || + (getterType == double.class && valueType == Double.class) || + (getterType == short.class && valueType == Short.class) || + (getterType == byte.class && valueType == Byte.class) || + (getterType == char.class && valueType == Character.class); + } + return false; + } + return true; + } + + @Override + public void set(T object, V value) { + if (mSetter != null) { + try { + mSetter.invoke(object, value); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getCause()); + } + } else if (mField != null) { + try { + mField.set(object, value); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } else { + throw new UnsupportedOperationException("Property " + getName() +" is read-only"); + } + } + + @Override + public V get(T object) { + if (mGetter != null) { + try { + return (V) mGetter.invoke(object, (Object[])null); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getCause()); + } + } else if (mField != null) { + try { + return (V) mField.get(object); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + // Should not get here: there should always be a non-null getter or field + throw new AssertionError(); + } + + /** + * Returns false if there is no setter or public field underlying this Property. + */ + @Override + public boolean isReadOnly() { + return (mSetter == null && mField == null); + } +} diff --git a/src/main/java/android/util/ScrollViewScenario.java b/src/main/java/android/util/ScrollViewScenario.java new file mode 100644 index 0000000..db3d9d0 --- /dev/null +++ b/src/main/java/android/util/ScrollViewScenario.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2008 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 android.util; + +import com.google.android.collect.Lists; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import java.util.List; + +/** + * Utility base class for creating scroll view scenarios, allowing you to add + * a series of different kinds of views arranged vertically, taking up a + * specified amount of the screen height. + */ +public abstract class ScrollViewScenario extends Activity { + + /** + * Holds content of scroll view + */ + private LinearLayout mLinearLayout; + + /** + * The actual scroll view + */ + private ScrollView mScrollView; + + + /** + * What we need of each view that the user wants: the view, and the ratio + * to the screen height for its desired height. + */ + private interface ViewFactory { + View create(final Context context); + + float getHeightRatio(); + } + + /** + * Partially implement ViewFactory given a height ratio. + * A negative height ratio means that WRAP_CONTENT will be used as height + */ + private static abstract class ViewFactoryBase implements ViewFactory { + + private float mHeightRatio; + + @SuppressWarnings({"UnusedDeclaration"}) + private ViewFactoryBase() {throw new UnsupportedOperationException("don't call this!");} + + protected ViewFactoryBase(float heightRatio) { + mHeightRatio = heightRatio; + } + + public float getHeightRatio() { + return mHeightRatio; + } + } + + /** + * Builder for selecting the views to be vertically arranged in the scroll + * view. + */ + @SuppressWarnings({"JavaDoc"}) + public static class Params { + + List mViewFactories = Lists.newArrayList(); + + int mTopPadding = 0; + int mBottomPadding = 0; + + /** + * Add a text view. + * @param text The text of the text view. + * @param heightRatio The view's height will be this * the screen height. + */ + public Params addTextView(final String text, float heightRatio) { + mViewFactories.add(new ViewFactoryBase(heightRatio) { + public View create(final Context context) { + final TextView tv = new TextView(context); + tv.setText(text); + return tv; + } + }); + return this; + } + + /** + * Add multiple text views. + * @param numViews the number of views to add. + * @param textPrefix The text to prepend to each text view. + * @param heightRatio The view's height will be this * the screen height. + */ + public Params addTextViews(int numViews, String textPrefix, float heightRatio) { + for (int i = 0; i < numViews; i++) { + addTextView(textPrefix + i, heightRatio); + } + return this; + } + + /** + * Add a button. + * @param text The text of the button. + * @param heightRatio The view's height will be this * the screen height. + */ + public Params addButton(final String text, float heightRatio) { + mViewFactories.add(new ViewFactoryBase(heightRatio) { + public View create(final Context context) { + final Button button = new Button(context); + button.setText(text); + return button; + } + }); + return this; + } + + /** + * Add multiple buttons. + * @param numButtons the number of views to add. + * @param textPrefix The text to prepend to each button. + * @param heightRatio The view's height will be this * the screen height. + */ + public Params addButtons(int numButtons, String textPrefix, float heightRatio) { + for (int i = 0; i < numButtons; i++) { + addButton(textPrefix + i, heightRatio); + } + return this; + } + + /** + * Add an {@link InternalSelectionView}. + * @param numRows The number of rows in the internal selection view. + * @param heightRatio The view's height will be this * the screen height. + */ + public Params addInternalSelectionView(final int numRows, float heightRatio) { + mViewFactories.add(new ViewFactoryBase(heightRatio) { + public View create(final Context context) { + return new InternalSelectionView(context, numRows, "isv"); + } + }); + return this; + } + + /** + * Add a sublayout of buttons as a single child of the scroll view. + * @param numButtons The number of buttons in the sub layout + * @param heightRatio The layout's height will be this * the screen height. + */ + public Params addVerticalLLOfButtons(final String prefix, final int numButtons, float heightRatio) { + mViewFactories.add(new ViewFactoryBase(heightRatio) { + + public View create(Context context) { + final LinearLayout ll = new LinearLayout(context); + ll.setOrientation(LinearLayout.VERTICAL); + + // fill width, equally weighted on height + final LinearLayout.LayoutParams lp = + new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f); + for (int i = 0; i < numButtons; i++) { + final Button button = new Button(context); + button.setText(prefix + i); + ll.addView(button, lp); + } + + return ll; + } + }); + return this; + } + + public Params addPaddingToScrollView(int topPadding, int bottomPadding) { + mTopPadding = topPadding; + mBottomPadding = bottomPadding; + + return this; + } + } + + /** + * Override this and initialized the views in the scroll view. + * @param params Used to configure the contents of the scroll view. + */ + protected abstract void init(Params params); + + public LinearLayout getLinearLayout() { + return mLinearLayout; + } + + public ScrollView getScrollView() { + return mScrollView; + } + + /** + * Get the child contained within the vertical linear layout of the + * scroll view. + * @param index The index within the linear layout. + * @return the child within the vertical linear layout of the scroll view + * at the specified index. + */ + @SuppressWarnings({"unchecked"}) + public T getContentChildAt(int index) { + return (T) mLinearLayout.getChildAt(index); + } + + /** + * Hook for changing how scroll view's are created. + */ + @SuppressWarnings({"JavaDoc"}) + protected ScrollView createScrollView() { + return new ScrollView(this); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // for test stability, turn off title bar + requestWindowFeature(Window.FEATURE_NO_TITLE); + int screenHeight = getWindowManager().getDefaultDisplay().getHeight() + - 25; + mLinearLayout = new LinearLayout(this); + mLinearLayout.setOrientation(LinearLayout.VERTICAL); + + // initialize params + final Params params = new Params(); + init(params); + + // create views specified by params + for (ViewFactory viewFactory : params.mViewFactories) { + int height = ViewGroup.LayoutParams.WRAP_CONTENT; + if (viewFactory.getHeightRatio() >= 0) { + height = (int) (viewFactory.getHeightRatio() * screenHeight); + } + final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, height); + mLinearLayout.addView(viewFactory.create(this), lp); + } + + mScrollView = createScrollView(); + mScrollView.setPadding(0, params.mTopPadding, 0, params.mBottomPadding); + mScrollView.addView(mLinearLayout, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + // no animation to speed up tests + mScrollView.setSmoothScrollingEnabled(false); + + setContentView(mScrollView); + } +} diff --git a/src/main/java/android/util/Singleton.java b/src/main/java/android/util/Singleton.java new file mode 100644 index 0000000..8a38bdb --- /dev/null +++ b/src/main/java/android/util/Singleton.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2010 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 android.util; + +/** + * Singleton helper class for lazily initialization. + * + * Modeled after frameworks/base/include/utils/Singleton.h + * + * @hide + */ +public abstract class Singleton { + private T mInstance; + + protected abstract T create(); + + public final T get() { + synchronized (this) { + if (mInstance == null) { + mInstance = create(); + } + return mInstance; + } + } +} diff --git a/src/main/java/android/util/Size.java b/src/main/java/android/util/Size.java new file mode 100644 index 0000000..62df564 --- /dev/null +++ b/src/main/java/android/util/Size.java @@ -0,0 +1,152 @@ +/* + * 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 android.util; + +import static com.android.internal.util.Preconditions.checkNotNull; + +/** + * Immutable class for describing width and height dimensions in pixels. + */ +public final class Size { + /** + * Create a new immutable Size instance. + * + * @param width The width of the size, in pixels + * @param height The height of the size, in pixels + */ + public Size(int width, int height) { + mWidth = width; + mHeight = height; + } + + /** + * Get the width of the size (in pixels). + * @return width + */ + public int getWidth() { + return mWidth; + } + + /** + * Get the height of the size (in pixels). + * @return height + */ + public int getHeight() { + return mHeight; + } + + /** + * Check if this size is equal to another size. + *

    + * Two sizes are equal if and only if both their widths and heights are + * equal. + *

    + *

    + * A size object is never equal to any other type of object. + *

    + * + * @return {@code true} if the objects were equal, {@code false} otherwise + */ + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj instanceof Size) { + Size other = (Size) obj; + return mWidth == other.mWidth && mHeight == other.mHeight; + } + return false; + } + + /** + * Return the size represented as a string with the format {@code "WxH"} + * + * @return string representation of the size + */ + @Override + public String toString() { + return mWidth + "x" + mHeight; + } + + private static NumberFormatException invalidSize(String s) { + throw new NumberFormatException("Invalid Size: \"" + s + "\""); + } + + /** + * Parses the specified string as a size value. + *

    + * The ASCII characters {@code \}{@code u002a} ('*') and + * {@code \}{@code u0078} ('x') are recognized as separators between + * the width and height.

    + *

    + * For any {@code Size s}: {@code Size.parseSize(s.toString()).equals(s)}. + * However, the method also handles sizes expressed in the + * following forms:

    + *

    + * "width{@code x}height" or + * "width{@code *}height" {@code => new Size(width, height)}, + * where width and height are string integers potentially + * containing a sign, such as "-10", "+7" or "5".

    + * + *
    {@code
    +     * Size.parseSize("3*+6").equals(new Size(3, 6)) == true
    +     * Size.parseSize("-3x-6").equals(new Size(-3, -6)) == true
    +     * Size.parseSize("4 by 3") => throws NumberFormatException
    +     * }
    + * + * @param string the string representation of a size value. + * @return the size value represented by {@code string}. + * + * @throws NumberFormatException if {@code string} cannot be parsed + * as a size value. + * @throws NullPointerException if {@code string} was {@code null} + */ + public static Size parseSize(String string) + throws NumberFormatException { + checkNotNull(string, "string must not be null"); + + int sep_ix = string.indexOf('*'); + if (sep_ix < 0) { + sep_ix = string.indexOf('x'); + } + if (sep_ix < 0) { + throw invalidSize(string); + } + try { + return new Size(Integer.parseInt(string.substring(0, sep_ix)), + Integer.parseInt(string.substring(sep_ix + 1))); + } catch (NumberFormatException e) { + throw invalidSize(string); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + // assuming most sizes are <2^16, doing a rotate will give us perfect hashing + return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2))); + } + + private final int mWidth; + private final int mHeight; +} diff --git a/src/main/java/android/util/SizeF.java b/src/main/java/android/util/SizeF.java new file mode 100644 index 0000000..2edc4a7 --- /dev/null +++ b/src/main/java/android/util/SizeF.java @@ -0,0 +1,164 @@ +/* + * 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 android.util; + +import static com.android.internal.util.Preconditions.checkNotNull; +import static com.android.internal.util.Preconditions.checkArgumentFinite; + +/** + * Immutable class for describing width and height dimensions in some arbitrary + * unit. + *

    + * Width and height are finite values stored as a floating point representation. + *

    + */ +public final class SizeF { + /** + * Create a new immutable SizeF instance. + * + *

    Both the {@code width} and the {@code height} must be a finite number. + * In particular, {@code NaN} and positive/negative infinity are illegal values.

    + * + * @param width The width of the size + * @param height The height of the size + * + * @throws IllegalArgumentException + * if either {@code width} or {@code height} was not finite. + */ + public SizeF(final float width, final float height) { + mWidth = checkArgumentFinite(width, "width"); + mHeight = checkArgumentFinite(height, "height"); + } + + /** + * Get the width of the size (as an arbitrary unit). + * @return width + */ + public float getWidth() { + return mWidth; + } + + /** + * Get the height of the size (as an arbitrary unit). + * @return height + */ + public float getHeight() { + return mHeight; + } + + /** + * Check if this size is equal to another size. + * + *

    Two sizes are equal if and only if both their widths and heights are the same.

    + * + *

    For this purpose, the width/height float values are considered to be the same if and only + * if the method {@link Float#floatToIntBits(float)} returns the identical {@code int} value + * when applied to each.

    + * + * @return {@code true} if the objects were equal, {@code false} otherwise + */ + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj instanceof SizeF) { + final SizeF other = (SizeF) obj; + return mWidth == other.mWidth && mHeight == other.mHeight; + } + return false; + } + + /** + * Return the size represented as a string with the format {@code "WxH"} + * + * @return string representation of the size + */ + @Override + public String toString() { + return mWidth + "x" + mHeight; + } + + private static NumberFormatException invalidSizeF(String s) { + throw new NumberFormatException("Invalid SizeF: \"" + s + "\""); + } + + /** + * Parses the specified string as a size value. + *

    + * The ASCII characters {@code \}{@code u002a} ('*') and + * {@code \}{@code u0078} ('x') are recognized as separators between + * the width and height.

    + *

    + * For any {@code SizeF s}: {@code SizeF.parseSizeF(s.toString()).equals(s)}. + * However, the method also handles sizes expressed in the + * following forms:

    + *

    + * "width{@code x}height" or + * "width{@code *}height" {@code => new SizeF(width, height)}, + * where width and height are string floats potentially + * containing a sign, such as "-10.3", "+7" or "5.2", but not containing + * an {@code 'x'} (such as a float in hexadecimal string format).

    + * + *
    {@code
    +     * SizeF.parseSizeF("3.2*+6").equals(new SizeF(3.2f, 6.0f)) == true
    +     * SizeF.parseSizeF("-3x-6").equals(new SizeF(-3.0f, -6.0f)) == true
    +     * SizeF.parseSizeF("4 by 3") => throws NumberFormatException
    +     * }
    + * + * @param string the string representation of a size value. + * @return the size value represented by {@code string}. + * + * @throws NumberFormatException if {@code string} cannot be parsed + * as a size value. + * @throws NullPointerException if {@code string} was {@code null} + */ + public static SizeF parseSizeF(String string) + throws NumberFormatException { + checkNotNull(string, "string must not be null"); + + int sep_ix = string.indexOf('*'); + if (sep_ix < 0) { + sep_ix = string.indexOf('x'); + } + if (sep_ix < 0) { + throw invalidSizeF(string); + } + try { + return new SizeF(Float.parseFloat(string.substring(0, sep_ix)), + Float.parseFloat(string.substring(sep_ix + 1))); + } catch (NumberFormatException e) { + throw invalidSizeF(string); + } catch (IllegalArgumentException e) { + throw invalidSizeF(string); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Float.floatToIntBits(mWidth) ^ Float.floatToIntBits(mHeight); + } + + private final float mWidth; + private final float mHeight; +} diff --git a/src/main/java/android/util/Slog.java b/src/main/java/android/util/Slog.java new file mode 100644 index 0000000..58a2703 --- /dev/null +++ b/src/main/java/android/util/Slog.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2006 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 android.util; + +/** + * @hide + */ +public final class Slog { + + private Slog() { + } + + public static int v(String tag, String msg) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.VERBOSE, tag, msg); + } + + public static int v(String tag, String msg, Throwable tr) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.VERBOSE, tag, + msg + '\n' + Log.getStackTraceString(tr)); + } + + public static int d(String tag, String msg) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.DEBUG, tag, msg); + } + + public static int d(String tag, String msg, Throwable tr) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.DEBUG, tag, + msg + '\n' + Log.getStackTraceString(tr)); + } + + public static int i(String tag, String msg) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.INFO, tag, msg); + } + + public static int i(String tag, String msg, Throwable tr) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.INFO, tag, + msg + '\n' + Log.getStackTraceString(tr)); + } + + public static int w(String tag, String msg) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.WARN, tag, msg); + } + + public static int w(String tag, String msg, Throwable tr) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.WARN, tag, + msg + '\n' + Log.getStackTraceString(tr)); + } + + public static int w(String tag, Throwable tr) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.WARN, tag, Log.getStackTraceString(tr)); + } + + public static int e(String tag, String msg) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.ERROR, tag, msg); + } + + public static int e(String tag, String msg, Throwable tr) { + return Log.println_native(Log.LOG_ID_SYSTEM, Log.ERROR, tag, + msg + '\n' + Log.getStackTraceString(tr)); + } + + /** + * Like {@link Log#wtf(String, String)}, but will never cause the caller to crash, and + * will always be handled asynchronously. Primarily for use by coding running within + * the system process. + */ + public static int wtf(String tag, String msg) { + return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, null, false, true); + } + + /** + * Like {@link #wtf(String, String)}, but does not output anything to the log. + */ + public static void wtfQuiet(String tag, String msg) { + Log.wtfQuiet(Log.LOG_ID_SYSTEM, tag, msg, true); + } + + /** + * Like {@link Log#wtfStack(String, String)}, but will never cause the caller to crash, and + * will always be handled asynchronously. Primarily for use by coding running within + * the system process. + */ + public static int wtfStack(String tag, String msg) { + return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, null, true, true); + } + + /** + * Like {@link Log#wtf(String, Throwable)}, but will never cause the caller to crash, + * and will always be handled asynchronously. Primarily for use by coding running within + * the system process. + */ + public static int wtf(String tag, Throwable tr) { + return Log.wtf(Log.LOG_ID_SYSTEM, tag, tr.getMessage(), tr, false, true); + } + + /** + * Like {@link Log#wtf(String, String, Throwable)}, but will never cause the caller to crash, + * and will always be handled asynchronously. Primarily for use by coding running within + * the system process. + */ + public static int wtf(String tag, String msg, Throwable tr) { + return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, tr, false, true); + } + + public static int println(int priority, String tag, String msg) { + return Log.println_native(Log.LOG_ID_SYSTEM, priority, tag, msg); + } +} + diff --git a/src/main/java/android/util/SparseArray.java b/src/main/java/android/util/SparseArray.java new file mode 100644 index 0000000..92e874f --- /dev/null +++ b/src/main/java/android/util/SparseArray.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2006 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 android.util; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; + +/** + * SparseArrays map integers to Objects. Unlike a normal array of Objects, + * there can be gaps in the indices. It is intended to be more memory efficient + * than using a HashMap to map Integers to Objects, both because it avoids + * auto-boxing keys and its data structure doesn't rely on an extra entry object + * for each mapping. + * + *

    Note that this container keeps its mappings in an array data structure, + * using a binary search to find keys. The implementation is not intended to be appropriate for + * data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    To help with performance, the container includes an optimization when removing + * keys: instead of compacting its array immediately, it leaves the removed entry marked + * as deleted. The entry can then be re-used for the same key, or compacted later in + * a single garbage collection step of all removed entries. This garbage collection will + * need to be performed at any time the array needs to be grown or the the map size or + * entry values are retrieved.

    + * + *

    It is possible to iterate over the items in this container using + * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using + * keyAt(int) with ascending values of the index will return the + * keys in ascending order, or the values corresponding to the keys in ascending + * order in the case of valueAt(int).

    + */ +public class SparseArray implements Cloneable { + private static final Object DELETED = new Object(); + private boolean mGarbage = false; + + private int[] mKeys; + private Object[] mValues; + private int mSize; + + /** + * Creates a new SparseArray containing no mappings. + */ + public SparseArray() { + this(10); + } + + /** + * Creates a new SparseArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public SparseArray(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = EmptyArray.INT; + mValues = EmptyArray.OBJECT; + } else { + mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity); + mKeys = new int[mValues.length]; + } + mSize = 0; + } + + @Override + @SuppressWarnings("unchecked") + public SparseArray clone() { + SparseArray clone = null; + try { + clone = (SparseArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the Object mapped from the specified key, or null + * if no such mapping has been made. + */ + public E get(int key) { + return get(key, null); + } + + /** + * Gets the Object mapped from the specified key, or the specified Object + * if no such mapping has been made. + */ + @SuppressWarnings("unchecked") + public E get(int key, E valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i < 0 || mValues[i] == DELETED) { + return valueIfKeyNotFound; + } else { + return (E) mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + if (mValues[i] != DELETED) { + mValues[i] = DELETED; + mGarbage = true; + } + } + } + + /** + * Alias for {@link #delete(int)}. + */ + public void remove(int key) { + delete(key); + } + + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + if (mValues[index] != DELETED) { + mValues[index] = DELETED; + mGarbage = true; + } + } + + /** + * Remove a range of mappings as a batch. + * + * @param index Index to begin at + * @param size Number of mappings to remove + */ + public void removeAtRange(int index, int size) { + final int end = Math.min(mSize, index + size); + for (int i = index; i < end; i++) { + removeAt(i); + } + } + + private void gc() { + // Log.e("SparseArray", "gc start with " + mSize); + + int n = mSize; + int o = 0; + int[] keys = mKeys; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + Object val = values[i]; + + if (val != DELETED) { + if (i != o) { + keys[o] = keys[i]; + values[o] = val; + values[i] = null; + } + + o++; + } + } + + mGarbage = false; + mSize = o; + + // Log.e("SparseArray", "gc end with " + mSize); + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, E value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + if (i < mSize && mValues[i] == DELETED) { + mKeys[i] = key; + mValues[i] = value; + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + + // Search again because indices may have changed. + i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseArray + * currently stores. + */ + public int size() { + if (mGarbage) { + gc(); + } + + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * SparseArray stores. + * + *

    The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., keyAt(0) will return the + * smallest key and keyAt(size()-1) will return the largest + * key.

    + */ + public int keyAt(int index) { + if (mGarbage) { + gc(); + } + + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * SparseArray stores. + * + *

    The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * valueAt(0) will return the value associated with the + * smallest key and valueAt(size()-1) will return the value + * associated with the largest key.

    + */ + @SuppressWarnings("unchecked") + public E valueAt(int index) { + if (mGarbage) { + gc(); + } + + return (E) mValues[index]; + } + + /** + * Given an index in the range 0...size()-1, sets a new + * value for the indexth key-value mapping that this + * SparseArray stores. + */ + public void setValueAt(int index, E value) { + if (mGarbage) { + gc(); + } + + mValues[index] = value; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + if (mGarbage) { + gc(); + } + + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + *

    Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + *

    Note also that unlike most collections' {@code indexOf} methods, + * this method compares values using {@code ==} rather than {@code equals}. + */ + public int indexOfValue(E value) { + if (mGarbage) { + gc(); + } + + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseArray. + */ + public void clear() { + int n = mSize; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + values[i] = null; + } + + mSize = 0; + mGarbage = false; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, E value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + } + + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; + } + + /** + * {@inheritDoc} + * + *

    This implementation composes a string by iterating over its mappings. If + * this map contains itself as a value, the string "(this Map)" + * will appear in its place. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + int key = keyAt(i); + buffer.append(key); + buffer.append('='); + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Map)"); + } + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/src/main/java/android/util/SparseBooleanArray.java b/src/main/java/android/util/SparseBooleanArray.java new file mode 100644 index 0000000..e293b1f --- /dev/null +++ b/src/main/java/android/util/SparseBooleanArray.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2006 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 android.util; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; + +/** + * SparseBooleanArrays map integers to booleans. + * Unlike a normal array of booleans + * there can be gaps in the indices. It is intended to be more memory efficient + * than using a HashMap to map Integers to Booleans, both because it avoids + * auto-boxing keys and values and its data structure doesn't rely on an extra entry object + * for each mapping. + * + *

    Note that this container keeps its mappings in an array data structure, + * using a binary search to find keys. The implementation is not intended to be appropriate for + * data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    It is possible to iterate over the items in this container using + * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using + * keyAt(int) with ascending values of the index will return the + * keys in ascending order, or the values corresponding to the keys in ascending + * order in the case of valueAt(int).

    + */ +public class SparseBooleanArray implements Cloneable { + /** + * Creates a new SparseBooleanArray containing no mappings. + */ + public SparseBooleanArray() { + this(10); + } + + /** + * Creates a new SparseBooleanArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public SparseBooleanArray(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = EmptyArray.INT; + mValues = EmptyArray.BOOLEAN; + } else { + mKeys = ArrayUtils.newUnpaddedIntArray(initialCapacity); + mValues = new boolean[mKeys.length]; + } + mSize = 0; + } + + @Override + public SparseBooleanArray clone() { + SparseBooleanArray clone = null; + try { + clone = (SparseBooleanArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the boolean mapped from the specified key, or false + * if no such mapping has been made. + */ + public boolean get(int key) { + return get(key, false); + } + + /** + * Gets the boolean mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public boolean get(int key, boolean valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i < 0) { + return valueIfKeyNotFound; + } else { + return mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + System.arraycopy(mKeys, i + 1, mKeys, i, mSize - (i + 1)); + System.arraycopy(mValues, i + 1, mValues, i, mSize - (i + 1)); + mSize--; + } + } + + /** @hide */ + public void removeAt(int index) { + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1)); + mSize--; + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, boolean value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseBooleanArray + * currently stores. + */ + public int size() { + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * SparseBooleanArray stores. + * + *

    The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., keyAt(0) will return the + * smallest key and keyAt(size()-1) will return the largest + * key.

    + */ + public int keyAt(int index) { + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * SparseBooleanArray stores. + * + *

    The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * valueAt(0) will return the value associated with the + * smallest key and valueAt(size()-1) will return the value + * associated with the largest key.

    + */ + public boolean valueAt(int index) { + return mValues[index]; + } + + /** @hide */ + public void setValueAt(int index, boolean value) { + mValues[index] = value; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(boolean value) { + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseBooleanArray. + */ + public void clear() { + mSize = 0; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, boolean value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; + } + + /** + * {@inheritDoc} + * + *

    This implementation composes a string by iterating over its mappings. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + int key = keyAt(i); + buffer.append(key); + buffer.append('='); + boolean value = valueAt(i); + buffer.append(value); + } + buffer.append('}'); + return buffer.toString(); + } + + private int[] mKeys; + private boolean[] mValues; + private int mSize; +} diff --git a/src/main/java/android/util/SparseIntArray.java b/src/main/java/android/util/SparseIntArray.java new file mode 100644 index 0000000..2b85a21 --- /dev/null +++ b/src/main/java/android/util/SparseIntArray.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2006 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 android.util; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; + +/** + * SparseIntArrays map integers to integers. Unlike a normal array of integers, + * there can be gaps in the indices. It is intended to be more memory efficient + * than using a HashMap to map Integers to Integers, both because it avoids + * auto-boxing keys and values and its data structure doesn't rely on an extra entry object + * for each mapping. + * + *

    Note that this container keeps its mappings in an array data structure, + * using a binary search to find keys. The implementation is not intended to be appropriate for + * data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    It is possible to iterate over the items in this container using + * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using + * keyAt(int) with ascending values of the index will return the + * keys in ascending order, or the values corresponding to the keys in ascending + * order in the case of valueAt(int).

    + */ +public class SparseIntArray implements Cloneable { + private int[] mKeys; + private int[] mValues; + private int mSize; + + /** + * Creates a new SparseIntArray containing no mappings. + */ + public SparseIntArray() { + this(10); + } + + /** + * Creates a new SparseIntArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public SparseIntArray(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = EmptyArray.INT; + mValues = EmptyArray.INT; + } else { + mKeys = ArrayUtils.newUnpaddedIntArray(initialCapacity); + mValues = new int[mKeys.length]; + } + mSize = 0; + } + + @Override + public SparseIntArray clone() { + SparseIntArray clone = null; + try { + clone = (SparseIntArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the int mapped from the specified key, or 0 + * if no such mapping has been made. + */ + public int get(int key) { + return get(key, 0); + } + + /** + * Gets the int mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public int get(int key, int valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i < 0) { + return valueIfKeyNotFound; + } else { + return mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + removeAt(i); + } + } + + /** + * Removes the mapping at the given index. + */ + public void removeAt(int index) { + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1)); + mSize--; + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, int value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseIntArray + * currently stores. + */ + public int size() { + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * SparseIntArray stores. + * + *

    The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., keyAt(0) will return the + * smallest key and keyAt(size()-1) will return the largest + * key.

    + */ + public int keyAt(int index) { + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * SparseIntArray stores. + * + *

    The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * valueAt(0) will return the value associated with the + * smallest key and valueAt(size()-1) will return the value + * associated with the largest key.

    + */ + public int valueAt(int index) { + return mValues[index]; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(int value) { + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseIntArray. + */ + public void clear() { + mSize = 0; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, int value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; + } + + /** + * {@inheritDoc} + * + *

    This implementation composes a string by iterating over its mappings. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + int key = keyAt(i); + buffer.append(key); + buffer.append('='); + int value = valueAt(i); + buffer.append(value); + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/src/main/java/android/util/SparseLongArray.java b/src/main/java/android/util/SparseLongArray.java new file mode 100644 index 0000000..0166c4a --- /dev/null +++ b/src/main/java/android/util/SparseLongArray.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2011 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 android.util; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import libcore.util.EmptyArray; + +/** + * SparseLongArrays map integers to longs. Unlike a normal array of longs, + * there can be gaps in the indices. It is intended to be more memory efficient + * than using a HashMap to map Integers to Longs, both because it avoids + * auto-boxing keys and values and its data structure doesn't rely on an extra entry object + * for each mapping. + * + *

    Note that this container keeps its mappings in an array data structure, + * using a binary search to find keys. The implementation is not intended to be appropriate for + * data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    It is possible to iterate over the items in this container using + * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using + * keyAt(int) with ascending values of the index will return the + * keys in ascending order, or the values corresponding to the keys in ascending + * order in the case of valueAt(int).

    + */ +public class SparseLongArray implements Cloneable { + private int[] mKeys; + private long[] mValues; + private int mSize; + + /** + * Creates a new SparseLongArray containing no mappings. + */ + public SparseLongArray() { + this(10); + } + + /** + * Creates a new SparseLongArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public SparseLongArray(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = EmptyArray.INT; + mValues = EmptyArray.LONG; + } else { + mValues = ArrayUtils.newUnpaddedLongArray(initialCapacity); + mKeys = new int[mValues.length]; + } + mSize = 0; + } + + @Override + public SparseLongArray clone() { + SparseLongArray clone = null; + try { + clone = (SparseLongArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the long mapped from the specified key, or 0 + * if no such mapping has been made. + */ + public long get(int key) { + return get(key, 0); + } + + /** + * Gets the long mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public long get(int key, long valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i < 0) { + return valueIfKeyNotFound; + } else { + return mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + removeAt(i); + } + } + + /** + * Removes the mapping at the given index. + */ + public void removeAt(int index) { + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1)); + mSize--; + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, long value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseIntArray + * currently stores. + */ + public int size() { + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * SparseLongArray stores. + * + *

    The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., keyAt(0) will return the + * smallest key and keyAt(size()-1) will return the largest + * key.

    + */ + public int keyAt(int index) { + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * SparseLongArray stores. + * + *

    The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * valueAt(0) will return the value associated with the + * smallest key and valueAt(size()-1) will return the value + * associated with the largest key.

    + */ + public long valueAt(int index) { + return mValues[index]; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(long value) { + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseIntArray. + */ + public void clear() { + mSize = 0; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, long value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + mKeys = GrowingArrayUtils.append(mKeys, mSize, key); + mValues = GrowingArrayUtils.append(mValues, mSize, value); + mSize++; + } + + /** + * {@inheritDoc} + * + *

    This implementation composes a string by iterating over its mappings. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + int key = keyAt(i); + buffer.append(key); + buffer.append('='); + long value = valueAt(i); + buffer.append(value); + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/src/main/java/android/util/Spline.java b/src/main/java/android/util/Spline.java new file mode 100644 index 0000000..41a2e5d --- /dev/null +++ b/src/main/java/android/util/Spline.java @@ -0,0 +1,297 @@ +/* + * 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 android.util; + +/** + * Performs spline interpolation given a set of control points. + * @hide + */ +public abstract class Spline { + + /** + * Interpolates the value of Y = f(X) for given X. + * Clamps X to the domain of the spline. + * + * @param x The X value. + * @return The interpolated Y = f(X) value. + */ + public abstract float interpolate(float x); + + /** + * Creates an appropriate spline based on the properties of the control points. + * + * If the control points are monotonic then the resulting spline will preserve that and + * otherwise optimize for error bounds. + */ + public static Spline createSpline(float[] x, float[] y) { + if (!isStrictlyIncreasing(x)) { + throw new IllegalArgumentException("The control points must all have strictly " + + "increasing X values."); + } + + if (isMonotonic(y)) { + return createMonotoneCubicSpline(x, y); + } else { + return createLinearSpline(x, y); + } + } + + /** + * Creates a monotone cubic spline from a given set of control points. + * + * The spline is guaranteed to pass through each control point exactly. + * Moreover, assuming the control points are monotonic (Y is non-decreasing or + * non-increasing) then the interpolated values will also be monotonic. + * + * This function uses the Fritsch-Carlson method for computing the spline parameters. + * http://en.wikipedia.org/wiki/Monotone_cubic_interpolation + * + * @param x The X component of the control points, strictly increasing. + * @param y The Y component of the control points, monotonic. + * @return + * + * @throws IllegalArgumentException if the X or Y arrays are null, have + * different lengths or have fewer than 2 values. + * @throws IllegalArgumentException if the control points are not monotonic. + */ + public static Spline createMonotoneCubicSpline(float[] x, float[] y) { + return new MonotoneCubicSpline(x, y); + } + + /** + * Creates a linear spline from a given set of control points. + * + * Like a monotone cubic spline, the interpolated curve will be monotonic if the control points + * are monotonic. + * + * @param x The X component of the control points, strictly increasing. + * @param y The Y component of the control points. + * @return + * + * @throws IllegalArgumentException if the X or Y arrays are null, have + * different lengths or have fewer than 2 values. + * @throws IllegalArgumentException if the X components of the control points are not strictly + * increasing. + */ + public static Spline createLinearSpline(float[] x, float[] y) { + return new LinearSpline(x, y); + } + + private static boolean isStrictlyIncreasing(float[] x) { + if (x == null || x.length < 2) { + throw new IllegalArgumentException("There must be at least two control points."); + } + float prev = x[0]; + for (int i = 1; i < x.length; i++) { + float curr = x[i]; + if (curr <= prev) { + return false; + } + prev = curr; + } + return true; + } + + private static boolean isMonotonic(float[] x) { + if (x == null || x.length < 2) { + throw new IllegalArgumentException("There must be at least two control points."); + } + float prev = x[0]; + for (int i = 1; i < x.length; i++) { + float curr = x[i]; + if (curr < prev) { + return false; + } + prev = curr; + } + return true; + } + + public static class MonotoneCubicSpline extends Spline { + private float[] mX; + private float[] mY; + private float[] mM; + + public MonotoneCubicSpline(float[] x, float[] y) { + if (x == null || y == null || x.length != y.length || x.length < 2) { + throw new IllegalArgumentException("There must be at least two control " + + "points and the arrays must be of equal length."); + } + + final int n = x.length; + float[] d = new float[n - 1]; // could optimize this out + float[] m = new float[n]; + + // Compute slopes of secant lines between successive points. + for (int i = 0; i < n - 1; i++) { + float h = x[i + 1] - x[i]; + if (h <= 0f) { + throw new IllegalArgumentException("The control points must all " + + "have strictly increasing X values."); + } + d[i] = (y[i + 1] - y[i]) / h; + } + + // Initialize the tangents as the average of the secants. + m[0] = d[0]; + for (int i = 1; i < n - 1; i++) { + m[i] = (d[i - 1] + d[i]) * 0.5f; + } + m[n - 1] = d[n - 2]; + + // Update the tangents to preserve monotonicity. + for (int i = 0; i < n - 1; i++) { + if (d[i] == 0f) { // successive Y values are equal + m[i] = 0f; + m[i + 1] = 0f; + } else { + float a = m[i] / d[i]; + float b = m[i + 1] / d[i]; + if (a < 0f || b < 0f) { + throw new IllegalArgumentException("The control points must have " + + "monotonic Y values."); + } + float h = FloatMath.hypot(a, b); + if (h > 9f) { + float t = 3f / h; + m[i] = t * a * d[i]; + m[i + 1] = t * b * d[i]; + } + } + } + + mX = x; + mY = y; + mM = m; + } + + @Override + public float interpolate(float x) { + // Handle the boundary cases. + final int n = mX.length; + if (Float.isNaN(x)) { + return x; + } + if (x <= mX[0]) { + return mY[0]; + } + if (x >= mX[n - 1]) { + return mY[n - 1]; + } + + // Find the index 'i' of the last point with smaller X. + // We know this will be within the spline due to the boundary tests. + int i = 0; + while (x >= mX[i + 1]) { + i += 1; + if (x == mX[i]) { + return mY[i]; + } + } + + // Perform cubic Hermite spline interpolation. + float h = mX[i + 1] - mX[i]; + float t = (x - mX[i]) / h; + return (mY[i] * (1 + 2 * t) + h * mM[i] * t) * (1 - t) * (1 - t) + + (mY[i + 1] * (3 - 2 * t) + h * mM[i + 1] * (t - 1)) * t * t; + } + + // For debugging. + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + final int n = mX.length; + str.append("MonotoneCubicSpline{["); + for (int i = 0; i < n; i++) { + if (i != 0) { + str.append(", "); + } + str.append("(").append(mX[i]); + str.append(", ").append(mY[i]); + str.append(": ").append(mM[i]).append(")"); + } + str.append("]}"); + return str.toString(); + } + } + + public static class LinearSpline extends Spline { + private final float[] mX; + private final float[] mY; + private final float[] mM; + + public LinearSpline(float[] x, float[] y) { + if (x == null || y == null || x.length != y.length || x.length < 2) { + throw new IllegalArgumentException("There must be at least two control " + + "points and the arrays must be of equal length."); + } + final int N = x.length; + mM = new float[N-1]; + for (int i = 0; i < N-1; i++) { + mM[i] = (y[i+1] - y[i]) / (x[i+1] - x[i]); + } + mX = x; + mY = y; + } + + @Override + public float interpolate(float x) { + // Handle the boundary cases. + final int n = mX.length; + if (Float.isNaN(x)) { + return x; + } + if (x <= mX[0]) { + return mY[0]; + } + if (x >= mX[n - 1]) { + return mY[n - 1]; + } + + // Find the index 'i' of the last point with smaller X. + // We know this will be within the spline due to the boundary tests. + int i = 0; + while (x >= mX[i + 1]) { + i += 1; + if (x == mX[i]) { + return mY[i]; + } + } + return mY[i] + mM[i] * (x - mX[i]); + } + + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + final int n = mX.length; + str.append("LinearSpline{["); + for (int i = 0; i < n; i++) { + if (i != 0) { + str.append(", "); + } + str.append("(").append(mX[i]); + str.append(", ").append(mY[i]); + if (i < n-1) { + str.append(": ").append(mM[i]); + } + str.append(")"); + } + str.append("]}"); + return str.toString(); + } + } +} diff --git a/src/main/java/android/util/StateSet.java b/src/main/java/android/util/StateSet.java new file mode 100644 index 0000000..2623638 --- /dev/null +++ b/src/main/java/android/util/StateSet.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2007 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 android.util; + +import com.android.internal.R; + +/** + * State sets are arrays of positive ints where each element + * represents the state of a {@link android.view.View} (e.g. focused, + * selected, visible, etc.). A {@link android.view.View} may be in + * one or more of those states. + * + * A state spec is an array of signed ints where each element + * represents a required (if positive) or an undesired (if negative) + * {@link android.view.View} state. + * + * Utils dealing with state sets. + * + * In theory we could encapsulate the state set and state spec arrays + * and not have static methods here but there is some concern about + * performance since these methods are called during view drawing. + */ + +public class StateSet { + /** @hide */ public StateSet() {} + + public static final int[] WILD_CARD = new int[0]; + public static final int[] NOTHING = new int[] { 0 }; + + /** + * Return whether the stateSetOrSpec is matched by all StateSets. + * + * @param stateSetOrSpec a state set or state spec. + */ + public static boolean isWildCard(int[] stateSetOrSpec) { + return stateSetOrSpec.length == 0 || stateSetOrSpec[0] == 0; + } + + /** + * Return whether the stateSet matches the desired stateSpec. + * + * @param stateSpec an array of required (if positive) or + * prohibited (if negative) {@link android.view.View} states. + * @param stateSet an array of {@link android.view.View} states + */ + public static boolean stateSetMatches(int[] stateSpec, int[] stateSet) { + if (stateSet == null) { + return (stateSpec == null || isWildCard(stateSpec)); + } + int stateSpecSize = stateSpec.length; + int stateSetSize = stateSet.length; + for (int i = 0; i < stateSpecSize; i++) { + int stateSpecState = stateSpec[i]; + if (stateSpecState == 0) { + // We've reached the end of the cases to match against. + return true; + } + final boolean mustMatch; + if (stateSpecState > 0) { + mustMatch = true; + } else { + // We use negative values to indicate must-NOT-match states. + mustMatch = false; + stateSpecState = -stateSpecState; + } + boolean found = false; + for (int j = 0; j < stateSetSize; j++) { + final int state = stateSet[j]; + if (state == 0) { + // We've reached the end of states to match. + if (mustMatch) { + // We didn't find this must-match state. + return false; + } else { + // Continue checking other must-not-match states. + break; + } + } + if (state == stateSpecState) { + if (mustMatch) { + found = true; + // Continue checking other other must-match states. + break; + } else { + // Any match of a must-not-match state returns false. + return false; + } + } + } + if (mustMatch && !found) { + // We've reached the end of states to match and we didn't + // find a must-match state. + return false; + } + } + return true; + } + + /** + * Return whether the state matches the desired stateSpec. + * + * @param stateSpec an array of required (if positive) or + * prohibited (if negative) {@link android.view.View} states. + * @param state a {@link android.view.View} state + */ + public static boolean stateSetMatches(int[] stateSpec, int state) { + int stateSpecSize = stateSpec.length; + for (int i = 0; i < stateSpecSize; i++) { + int stateSpecState = stateSpec[i]; + if (stateSpecState == 0) { + // We've reached the end of the cases to match against. + return true; + } + if (stateSpecState > 0) { + if (state != stateSpecState) { + return false; + } + } else { + // We use negative values to indicate must-NOT-match states. + if (state == -stateSpecState) { + // We matched a must-not-match case. + return false; + } + } + } + return true; + } + + public static int[] trimStateSet(int[] states, int newSize) { + if (states.length == newSize) { + return states; + } + + int[] trimmedStates = new int[newSize]; + System.arraycopy(states, 0, trimmedStates, 0, newSize); + return trimmedStates; + } + + public static String dump(int[] states) { + StringBuilder sb = new StringBuilder(); + + int count = states.length; + for (int i = 0; i < count; i++) { + + switch (states[i]) { + case R.attr.state_window_focused: + sb.append("W "); + break; + case R.attr.state_pressed: + sb.append("P "); + break; + case R.attr.state_selected: + sb.append("S "); + break; + case R.attr.state_focused: + sb.append("F "); + break; + case R.attr.state_enabled: + sb.append("E "); + break; + } + } + + return sb.toString(); + } +} diff --git a/src/main/java/android/util/StateSetTest.java b/src/main/java/android/util/StateSetTest.java new file mode 100644 index 0000000..e481ce0 --- /dev/null +++ b/src/main/java/android/util/StateSetTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2007 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 android.util; + +import junit.framework.TestCase; +import android.test.suitebuilder.annotation.SmallTest; + +/** + * Tests for {@link StateSet} + */ + +public class StateSetTest extends TestCase { + + @SmallTest + public void testStateSetPositiveMatches() throws Exception { + int[] stateSpec = new int[2]; + int[] stateSet = new int[3]; + // Single states in both sets - match + stateSpec[0] = 1; + stateSet[0] = 1; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet)); + // Single states in both sets - non-match + stateSet[0] = 2; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet)); + // Add another state to the spec which the stateSet doesn't match + stateSpec[1] = 2; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet)); + // Add the missing matching element to the stateSet + stateSet[1] = 1; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet)); + // Add an irrelevent state to the stateSpec + stateSet[2] = 12345; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet)); + } + + @SmallTest + public void testStatesSetMatchMixEmUp() throws Exception { + int[] stateSpec = new int[2]; + int[] stateSet = new int[2]; + // One element in stateSpec which we must match and one which we must + // not match. stateSet only contains the match. + stateSpec[0] = 1; + stateSpec[1] = -2; + stateSet[0] = 1; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet)); + // stateSet now contains just the element we must not match + stateSet[0] = 2; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet)); + // Add another matching state to the the stateSet. We still fail + // because stateSet contains a must-not-match element + stateSet[1] = 1; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet)); + // Switch the must-not-match element in stateSet with a don't care + stateSet[0] = 12345; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet)); + } + + @SmallTest + public void testStateSetNegativeMatches() throws Exception { + int[] stateSpec = new int[2]; + int[] stateSet = new int[3]; + // Single states in both sets - match + stateSpec[0] = -1; + stateSet[0] = 2; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet)); + // Add another arrelevent state to the stateSet + stateSet[1] = 12345; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet)); + // Single states in both sets - non-match + stateSet[0] = 1; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet)); + // Add another state to the spec which the stateSet doesn't match + stateSpec[1] = -2; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet)); + // Add an irrelevent state to the stateSet + stateSet[2] = 12345; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet)); + } + + @SmallTest + public void testEmptySetMatchesNegtives() throws Exception { + int[] stateSpec = {-12345, -6789}; + int[] stateSet = new int[0]; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet)); + int[] stateSet2 = {0}; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet2)); + } + + @SmallTest + public void testEmptySetFailsPositives() throws Exception { + int[] stateSpec = {12345}; + int[] stateSet = new int[0]; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet)); + int[] stateSet2 = {0}; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet2)); + } + + @SmallTest + public void testEmptySetMatchesWildcard() throws Exception { + int[] stateSpec = StateSet.WILD_CARD; + int[] stateSet = new int[0]; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet)); + int[] stateSet2 = {0}; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet2)); + } + + @SmallTest + public void testSingleStatePositiveMatches() throws Exception { + int[] stateSpec = new int[2]; + int state; + // match + stateSpec[0] = 1; + state = 1; + assertTrue(StateSet.stateSetMatches(stateSpec, state)); + // non-match + state = 2; + assertFalse(StateSet.stateSetMatches(stateSpec, state)); + // add irrelevant must-not-match + stateSpec[1] = -12345; + assertFalse(StateSet.stateSetMatches(stateSpec, state)); + } + + @SmallTest + public void testSingleStateNegativeMatches() throws Exception { + int[] stateSpec = new int[2]; + int state; + // match + stateSpec[0] = -1; + state = 1; + assertFalse(StateSet.stateSetMatches(stateSpec, state)); + // non-match + state = 2; + assertTrue(StateSet.stateSetMatches(stateSpec, state)); + // add irrelevant must-not-match + stateSpec[1] = -12345; + assertTrue(StateSet.stateSetMatches(stateSpec, state)); + } + + @SmallTest + public void testZeroStateOnlyMatchesDefault() throws Exception { + int[] stateSpec = new int[3]; + int state = 0; + // non-match + stateSpec[0] = 1; + assertFalse(StateSet.stateSetMatches(stateSpec, state)); + // non-match + stateSpec[1] = -1; + assertFalse(StateSet.stateSetMatches(stateSpec, state)); + // match + stateSpec = StateSet.WILD_CARD; + assertTrue(StateSet.stateSetMatches(stateSpec, state)); + } + + @SmallTest + public void testNullStateOnlyMatchesDefault() throws Exception { + int[] stateSpec = new int[3]; + int[] stateSet = null; + // non-match + stateSpec[0] = 1; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet)); + // non-match + stateSpec[1] = -1; + assertFalse(StateSet.stateSetMatches(stateSpec, stateSet)); + // match + stateSpec = StateSet.WILD_CARD; + assertTrue(StateSet.stateSetMatches(stateSpec, stateSet)); + } +} diff --git a/src/main/java/android/util/StringBuilderPrinter.java b/src/main/java/android/util/StringBuilderPrinter.java new file mode 100644 index 0000000..d0fc1e7 --- /dev/null +++ b/src/main/java/android/util/StringBuilderPrinter.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2006 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 android.util; + +/** + * Implementation of a {@link android.util.Printer} that sends its output + * to a {@link StringBuilder}. + */ +public class StringBuilderPrinter implements Printer { + private final StringBuilder mBuilder; + + /** + * Create a new Printer that sends to a StringBuilder object. + * + * @param builder The StringBuilder where you would like output to go. + */ + public StringBuilderPrinter(StringBuilder builder) { + mBuilder = builder; + } + + public void println(String x) { + mBuilder.append(x); + int len = x.length(); + if (len <= 0 || x.charAt(len-1) != '\n') { + mBuilder.append('\n'); + } + } +} diff --git a/src/main/java/android/util/SuperNotCalledException.java b/src/main/java/android/util/SuperNotCalledException.java new file mode 100644 index 0000000..1836142 --- /dev/null +++ b/src/main/java/android/util/SuperNotCalledException.java @@ -0,0 +1,27 @@ +/* + * 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 android.util; + +/** + * @hide + */ +public final class SuperNotCalledException extends AndroidRuntimeException { + public SuperNotCalledException(String msg) { + super(msg); + } +} diff --git a/src/main/java/android/util/TimeFormatException.java b/src/main/java/android/util/TimeFormatException.java new file mode 100644 index 0000000..f520523 --- /dev/null +++ b/src/main/java/android/util/TimeFormatException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2006 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 android.util; + +public class TimeFormatException extends RuntimeException +{ + + /** + * @hide + */ + public TimeFormatException(String s) + { + super(s); + } +} + diff --git a/src/main/java/android/util/TimeUtils.java b/src/main/java/android/util/TimeUtils.java new file mode 100644 index 0000000..f7d2821 --- /dev/null +++ b/src/main/java/android/util/TimeUtils.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2006 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 android.util; + +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.os.SystemClock; +import android.text.format.DateUtils; + +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.TimeZone; + +import libcore.util.ZoneInfoDB; + +/** + * A class containing utility methods related to time zones. + */ +public class TimeUtils { + /** @hide */ public TimeUtils() {} + private static final boolean DBG = false; + private static final String TAG = "TimeUtils"; + + /** Cached results of getTineZones */ + private static final Object sLastLockObj = new Object(); + private static ArrayList sLastZones = null; + private static String sLastCountry = null; + + /** Cached results of getTimeZonesWithUniqueOffsets */ + private static final Object sLastUniqueLockObj = new Object(); + private static ArrayList sLastUniqueZoneOffsets = null; + private static String sLastUniqueCountry = null; + + + /** + * Tries to return a time zone that would have had the specified offset + * and DST value at the specified moment in the specified country. + * Returns null if no suitable zone could be found. + */ + public static TimeZone getTimeZone(int offset, boolean dst, long when, String country) { + TimeZone best = null; + final Date d = new Date(when); + + TimeZone current = TimeZone.getDefault(); + String currentName = current.getID(); + int currentOffset = current.getOffset(when); + boolean currentDst = current.inDaylightTime(d); + + for (TimeZone tz : getTimeZones(country)) { + // If the current time zone is from the right country + // and meets the other known properties, keep it + // instead of changing to another one. + + if (tz.getID().equals(currentName)) { + if (currentOffset == offset && currentDst == dst) { + return current; + } + } + + // Otherwise, take the first zone from the right + // country that has the correct current offset and DST. + // (Keep iterating instead of returning in case we + // haven't encountered the current time zone yet.) + + if (best == null) { + if (tz.getOffset(when) == offset && + tz.inDaylightTime(d) == dst) { + best = tz; + } + } + } + + return best; + } + + /** + * Return list of unique time zones for the country. Do not modify + * + * @param country to find + * @return list of unique time zones, maybe empty but never null. Do not modify. + * @hide + */ + public static ArrayList getTimeZonesWithUniqueOffsets(String country) { + synchronized(sLastUniqueLockObj) { + if ((country != null) && country.equals(sLastUniqueCountry)) { + if (DBG) { + Log.d(TAG, "getTimeZonesWithUniqueOffsets(" + + country + "): return cached version"); + } + return sLastUniqueZoneOffsets; + } + } + + Collection zones = getTimeZones(country); + ArrayList uniqueTimeZones = new ArrayList(); + for (TimeZone zone : zones) { + // See if we already have this offset, + // Using slow but space efficient and these are small. + boolean found = false; + for (int i = 0; i < uniqueTimeZones.size(); i++) { + if (uniqueTimeZones.get(i).getRawOffset() == zone.getRawOffset()) { + found = true; + break; + } + } + if (found == false) { + if (DBG) { + Log.d(TAG, "getTimeZonesWithUniqueOffsets: add unique offset=" + + zone.getRawOffset() + " zone.getID=" + zone.getID()); + } + uniqueTimeZones.add(zone); + } + } + + synchronized(sLastUniqueLockObj) { + // Cache the last result + sLastUniqueZoneOffsets = uniqueTimeZones; + sLastUniqueCountry = country; + + return sLastUniqueZoneOffsets; + } + } + + /** + * Returns the time zones for the country, which is the code + * attribute of the timezone element in time_zones_by_country.xml. Do not modify. + * + * @param country is a two character country code. + * @return TimeZone list, maybe empty but never null. Do not modify. + * @hide + */ + public static ArrayList getTimeZones(String country) { + synchronized (sLastLockObj) { + if ((country != null) && country.equals(sLastCountry)) { + if (DBG) Log.d(TAG, "getTimeZones(" + country + "): return cached version"); + return sLastZones; + } + } + + ArrayList tzs = new ArrayList(); + + if (country == null) { + if (DBG) Log.d(TAG, "getTimeZones(null): return empty list"); + return tzs; + } + + Resources r = Resources.getSystem(); + XmlResourceParser parser = r.getXml(com.android.internal.R.xml.time_zones_by_country); + + try { + XmlUtils.beginDocument(parser, "timezones"); + + while (true) { + XmlUtils.nextElement(parser); + + String element = parser.getName(); + if (element == null || !(element.equals("timezone"))) { + break; + } + + String code = parser.getAttributeValue(null, "code"); + + if (country.equals(code)) { + if (parser.next() == XmlPullParser.TEXT) { + String zoneIdString = parser.getText(); + TimeZone tz = TimeZone.getTimeZone(zoneIdString); + if (tz.getID().startsWith("GMT") == false) { + // tz.getID doesn't start not "GMT" so its valid + tzs.add(tz); + if (DBG) { + Log.d(TAG, "getTimeZone('" + country + "'): found tz.getID==" + + ((tz != null) ? tz.getID() : "")); + } + } + } + } + } + } catch (XmlPullParserException e) { + Log.e(TAG, "Got xml parser exception getTimeZone('" + country + "'): e=", e); + } catch (IOException e) { + Log.e(TAG, "Got IO exception getTimeZone('" + country + "'): e=", e); + } finally { + parser.close(); + } + + synchronized(sLastLockObj) { + // Cache the last result; + sLastZones = tzs; + sLastCountry = country; + return sLastZones; + } + } + + /** + * Returns a String indicating the version of the time zone database currently + * in use. The format of the string is dependent on the underlying time zone + * database implementation, but will typically contain the year in which the database + * was updated plus a letter from a to z indicating changes made within that year. + * + *

    Time zone database updates should be expected to occur periodically due to + * political and legal changes that cannot be anticipated in advance. Therefore, + * when computing the UTC time for a future event, applications should be aware that + * the results may differ following a time zone database update. This method allows + * applications to detect that a database change has occurred, and to recalculate any + * cached times accordingly. + * + *

    The time zone database may be assumed to change only when the device runtime + * is restarted. Therefore, it is not necessary to re-query the database version + * during the lifetime of an activity. + */ + public static String getTimeZoneDatabaseVersion() { + return ZoneInfoDB.getInstance().getVersion(); + } + + /** @hide Field length that can hold 999 days of time */ + public static final int HUNDRED_DAY_FIELD_LEN = 19; + + private static final int SECONDS_PER_MINUTE = 60; + private static final int SECONDS_PER_HOUR = 60 * 60; + private static final int SECONDS_PER_DAY = 24 * 60 * 60; + + /** @hide */ + public static final long NANOS_PER_MS = 1000000; + + private static final Object sFormatSync = new Object(); + private static char[] sFormatStr = new char[HUNDRED_DAY_FIELD_LEN+5]; + + private static final long LARGEST_DURATION = (1000 * DateUtils.DAY_IN_MILLIS) - 1; + + static private int accumField(int amt, int suffix, boolean always, int zeropad) { + if (amt > 99 || (always && zeropad >= 3)) { + return 3+suffix; + } + if (amt > 9 || (always && zeropad >= 2)) { + return 2+suffix; + } + if (always || amt > 0) { + return 1+suffix; + } + return 0; + } + + static private int printField(char[] formatStr, int amt, char suffix, int pos, + boolean always, int zeropad) { + if (always || amt > 0) { + final int startPos = pos; + if ((always && zeropad >= 3) || amt > 99) { + int dig = amt/100; + formatStr[pos] = (char)(dig + '0'); + pos++; + amt -= (dig*100); + } + if ((always && zeropad >= 2) || amt > 9 || startPos != pos) { + int dig = amt/10; + formatStr[pos] = (char)(dig + '0'); + pos++; + amt -= (dig*10); + } + formatStr[pos] = (char)(amt + '0'); + pos++; + formatStr[pos] = suffix; + pos++; + } + return pos; + } + + private static int formatDurationLocked(long duration, int fieldLen) { + if (sFormatStr.length < fieldLen) { + sFormatStr = new char[fieldLen]; + } + + char[] formatStr = sFormatStr; + + if (duration == 0) { + int pos = 0; + fieldLen -= 1; + while (pos < fieldLen) { + formatStr[pos++] = ' '; + } + formatStr[pos] = '0'; + return pos+1; + } + + char prefix; + if (duration > 0) { + prefix = '+'; + } else { + prefix = '-'; + duration = -duration; + } + + if (duration > LARGEST_DURATION) { + duration = LARGEST_DURATION; + } + + int millis = (int)(duration%1000); + int seconds = (int) Math.floor(duration / 1000); + int days = 0, hours = 0, minutes = 0; + + if (seconds > SECONDS_PER_DAY) { + days = seconds / SECONDS_PER_DAY; + seconds -= days * SECONDS_PER_DAY; + } + if (seconds > SECONDS_PER_HOUR) { + hours = seconds / SECONDS_PER_HOUR; + seconds -= hours * SECONDS_PER_HOUR; + } + if (seconds > SECONDS_PER_MINUTE) { + minutes = seconds / SECONDS_PER_MINUTE; + seconds -= minutes * SECONDS_PER_MINUTE; + } + + int pos = 0; + + if (fieldLen != 0) { + int myLen = accumField(days, 1, false, 0); + myLen += accumField(hours, 1, myLen > 0, 2); + myLen += accumField(minutes, 1, myLen > 0, 2); + myLen += accumField(seconds, 1, myLen > 0, 2); + myLen += accumField(millis, 2, true, myLen > 0 ? 3 : 0) + 1; + while (myLen < fieldLen) { + formatStr[pos] = ' '; + pos++; + myLen++; + } + } + + formatStr[pos] = prefix; + pos++; + + int start = pos; + boolean zeropad = fieldLen != 0; + pos = printField(formatStr, days, 'd', pos, false, 0); + pos = printField(formatStr, hours, 'h', pos, pos != start, zeropad ? 2 : 0); + pos = printField(formatStr, minutes, 'm', pos, pos != start, zeropad ? 2 : 0); + pos = printField(formatStr, seconds, 's', pos, pos != start, zeropad ? 2 : 0); + pos = printField(formatStr, millis, 'm', pos, true, (zeropad && pos != start) ? 3 : 0); + formatStr[pos] = 's'; + return pos + 1; + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long duration, StringBuilder builder) { + synchronized (sFormatSync) { + int len = formatDurationLocked(duration, 0); + builder.append(sFormatStr, 0, len); + } + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long duration, PrintWriter pw, int fieldLen) { + synchronized (sFormatSync) { + int len = formatDurationLocked(duration, fieldLen); + pw.print(new String(sFormatStr, 0, len)); + } + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long duration, PrintWriter pw) { + formatDuration(duration, pw, 0); + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long time, long now, PrintWriter pw) { + if (time == 0) { + pw.print("--"); + return; + } + formatDuration(time-now, pw, 0); + } + + /** @hide Just for debugging; not internationalized. */ + public static String formatUptime(long time) { + final long diff = time - SystemClock.uptimeMillis(); + if (diff > 0) { + return time + " (in " + diff + " ms)"; + } + if (diff < 0) { + return time + " (" + -diff + " ms ago)"; + } + return time + " (now)"; + } + + /** + * Convert a System.currentTimeMillis() value to a time of day value like + * that printed in logs. MM-DD HH:MM:SS.MMM + * + * @param millis since the epoch (1/1/1970) + * @return String representation of the time. + * @hide + */ + public static String logTimeOfDay(long millis) { + Calendar c = Calendar.getInstance(); + if (millis >= 0) { + c.setTimeInMillis(millis); + return String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c); + } else { + return Long.toString(millis); + } + } +} diff --git a/src/main/java/android/util/TimeUtilsTest.java b/src/main/java/android/util/TimeUtilsTest.java new file mode 100644 index 0000000..74c8e04 --- /dev/null +++ b/src/main/java/android/util/TimeUtilsTest.java @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * 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 android.util; + +import junit.framework.TestCase; + +import java.util.Calendar; +import java.util.TimeZone; + +/** + * TimeUtilsTest tests the time zone guesser. + */ +public class TimeUtilsTest extends TestCase { + public void testMainstream() throws Exception { + String[] mainstream = new String[] { + "America/New_York", // Eastern + "America/Chicago", // Central + "America/Denver", // Mountain + "America/Los_Angeles", // Pacific + "America/Anchorage", // Alaska + "Pacific/Honolulu", // Hawaii, no DST + }; + + for (String name : mainstream) { + TimeZone tz = TimeZone.getTimeZone(name); + Calendar c = Calendar.getInstance(tz); + TimeZone guess; + + c.set(2008, Calendar.OCTOBER, 20, 12, 00, 00); + guess = guess(c, "us"); + assertEquals(name, guess.getID()); + + c.set(2009, Calendar.JANUARY, 20, 12, 00, 00); + guess = guess(c, "us"); + assertEquals(name, guess.getID()); + } + } + + public void testWeird() throws Exception { + String[] weird = new String[] { + "America/Phoenix", // Mountain, no DST + "America/Adak", // Same as Hawaii, but with DST + }; + + for (String name : weird) { + TimeZone tz = TimeZone.getTimeZone(name); + Calendar c = Calendar.getInstance(tz); + TimeZone guess; + + c.set(2008, Calendar.OCTOBER, 20, 12, 00, 00); + guess = guess(c, "us"); + assertEquals(name, guess.getID()); + } + } + + public void testOld() throws Exception { + String[] old = new String[] { + "America/Indiana/Indianapolis", // Eastern, formerly no DST + }; + + for (String name : old) { + TimeZone tz = TimeZone.getTimeZone(name); + Calendar c = Calendar.getInstance(tz); + TimeZone guess; + + c.set(2005, Calendar.OCTOBER, 20, 12, 00, 00); + guess = guess(c, "us"); + assertEquals(name, guess.getID()); + } + } + + public void testWorld() throws Exception { + String[] world = new String[] { + "ad", "Europe/Andorra", + "ae", "Asia/Dubai", + "af", "Asia/Kabul", + "ag", "America/Antigua", + "ai", "America/Anguilla", + "al", "Europe/Tirane", + "am", "Asia/Yerevan", + "an", "America/Curacao", + "ao", "Africa/Luanda", + "aq", "Antarctica/McMurdo", + "aq", "Antarctica/DumontDUrville", + "aq", "Antarctica/Casey", + "aq", "Antarctica/Davis", + "aq", "Antarctica/Mawson", + "aq", "Antarctica/Syowa", + "aq", "Antarctica/Rothera", + "aq", "Antarctica/Palmer", + "ar", "America/Argentina/Buenos_Aires", + "as", "Pacific/Pago_Pago", + "at", "Europe/Vienna", + "au", "Australia/Sydney", + "au", "Australia/Adelaide", + "au", "Australia/Perth", + "au", "Australia/Eucla", + "aw", "America/Aruba", + "ax", "Europe/Mariehamn", + "az", "Asia/Baku", + "ba", "Europe/Sarajevo", + "bb", "America/Barbados", + "bd", "Asia/Dhaka", + "be", "Europe/Brussels", + "bf", "Africa/Ouagadougou", + "bg", "Europe/Sofia", + "bh", "Asia/Bahrain", + "bi", "Africa/Bujumbura", + "bj", "Africa/Porto-Novo", + "bm", "Atlantic/Bermuda", + "bn", "Asia/Brunei", + "bo", "America/La_Paz", + "br", "America/Noronha", + "br", "America/Sao_Paulo", + "br", "America/Manaus", + "bs", "America/Nassau", + "bt", "Asia/Thimphu", + "bw", "Africa/Gaborone", + "by", "Europe/Minsk", + "bz", "America/Belize", + "ca", "America/St_Johns", + "ca", "America/Halifax", + "ca", "America/Toronto", + "ca", "America/Winnipeg", + "ca", "America/Edmonton", + "ca", "America/Vancouver", + "cc", "Indian/Cocos", + "cd", "Africa/Lubumbashi", + "cd", "Africa/Kinshasa", + "cf", "Africa/Bangui", + "cg", "Africa/Brazzaville", + "ch", "Europe/Zurich", + "ci", "Africa/Abidjan", + "ck", "Pacific/Rarotonga", + "cl", "America/Santiago", + "cl", "Pacific/Easter", + "cm", "Africa/Douala", + "cn", "Asia/Shanghai", + "co", "America/Bogota", + "cr", "America/Costa_Rica", + "cu", "America/Havana", + "cv", "Atlantic/Cape_Verde", + "cx", "Indian/Christmas", + "cy", "Asia/Nicosia", + "cz", "Europe/Prague", + "de", "Europe/Berlin", + "dj", "Africa/Djibouti", + "dk", "Europe/Copenhagen", + "dm", "America/Dominica", + "do", "America/Santo_Domingo", + "dz", "Africa/Algiers", + "ec", "America/Guayaquil", + "ec", "Pacific/Galapagos", + "ee", "Europe/Tallinn", + "eg", "Africa/Cairo", + "eh", "Africa/El_Aaiun", + "er", "Africa/Asmara", + "es", "Europe/Madrid", + "es", "Atlantic/Canary", + "et", "Africa/Addis_Ababa", + "fi", "Europe/Helsinki", + "fj", "Pacific/Fiji", + "fk", "Atlantic/Stanley", + "fm", "Pacific/Ponape", + "fm", "Pacific/Truk", + "fo", "Atlantic/Faroe", + "fr", "Europe/Paris", + "ga", "Africa/Libreville", + "gb", "Europe/London", + "gd", "America/Grenada", + "ge", "Asia/Tbilisi", + "gf", "America/Cayenne", + "gg", "Europe/Guernsey", + "gh", "Africa/Accra", + "gi", "Europe/Gibraltar", + "gl", "America/Danmarkshavn", + "gl", "America/Scoresbysund", + "gl", "America/Godthab", + "gl", "America/Thule", + "gm", "Africa/Banjul", + "gn", "Africa/Conakry", + "gp", "America/Guadeloupe", + "gq", "Africa/Malabo", + "gr", "Europe/Athens", + "gs", "Atlantic/South_Georgia", + "gt", "America/Guatemala", + "gu", "Pacific/Guam", + "gw", "Africa/Bissau", + "gy", "America/Guyana", + "hk", "Asia/Hong_Kong", + "hn", "America/Tegucigalpa", + "hr", "Europe/Zagreb", + "ht", "America/Port-au-Prince", + "hu", "Europe/Budapest", + "id", "Asia/Jayapura", + "id", "Asia/Makassar", + "id", "Asia/Jakarta", + "ie", "Europe/Dublin", + "il", "Asia/Jerusalem", + "im", "Europe/Isle_of_Man", + "in", "Asia/Calcutta", + "io", "Indian/Chagos", + "iq", "Asia/Baghdad", + "ir", "Asia/Tehran", + "is", "Atlantic/Reykjavik", + "it", "Europe/Rome", + "je", "Europe/Jersey", + "jm", "America/Jamaica", + "jo", "Asia/Amman", + "jp", "Asia/Tokyo", + "ke", "Africa/Nairobi", + "kg", "Asia/Bishkek", + "kh", "Asia/Phnom_Penh", + "ki", "Pacific/Kiritimati", + "ki", "Pacific/Enderbury", + "ki", "Pacific/Tarawa", + "km", "Indian/Comoro", + "kn", "America/St_Kitts", + "kp", "Asia/Pyongyang", + "kr", "Asia/Seoul", + "kw", "Asia/Kuwait", + "ky", "America/Cayman", + "kz", "Asia/Almaty", + "kz", "Asia/Aqtau", + "la", "Asia/Vientiane", + "lb", "Asia/Beirut", + "lc", "America/St_Lucia", + "li", "Europe/Vaduz", + "lk", "Asia/Colombo", + "lr", "Africa/Monrovia", + "ls", "Africa/Maseru", + "lt", "Europe/Vilnius", + "lu", "Europe/Luxembourg", + "lv", "Europe/Riga", + "ly", "Africa/Tripoli", + "ma", "Africa/Casablanca", + "mc", "Europe/Monaco", + "md", "Europe/Chisinau", + "me", "Europe/Podgorica", + "mg", "Indian/Antananarivo", + "mh", "Pacific/Majuro", + "mk", "Europe/Skopje", + "ml", "Africa/Bamako", + "mm", "Asia/Rangoon", + "mn", "Asia/Choibalsan", + "mn", "Asia/Hovd", + "mo", "Asia/Macau", + "mp", "Pacific/Saipan", + "mq", "America/Martinique", + "mr", "Africa/Nouakchott", + "ms", "America/Montserrat", + "mt", "Europe/Malta", + "mu", "Indian/Mauritius", + "mv", "Indian/Maldives", + "mw", "Africa/Blantyre", + "mx", "America/Mexico_City", + "mx", "America/Chihuahua", + "mx", "America/Tijuana", + "my", "Asia/Kuala_Lumpur", + "mz", "Africa/Maputo", + "na", "Africa/Windhoek", + "nc", "Pacific/Noumea", + "ne", "Africa/Niamey", + "nf", "Pacific/Norfolk", + "ng", "Africa/Lagos", + "ni", "America/Managua", + "nl", "Europe/Amsterdam", + "no", "Europe/Oslo", + "np", "Asia/Katmandu", + "nr", "Pacific/Nauru", + "nu", "Pacific/Niue", + "nz", "Pacific/Auckland", + "nz", "Pacific/Chatham", + "om", "Asia/Muscat", + "pa", "America/Panama", + "pe", "America/Lima", + "pf", "Pacific/Gambier", + "pf", "Pacific/Marquesas", + "pf", "Pacific/Tahiti", + "pg", "Pacific/Port_Moresby", + "ph", "Asia/Manila", + "pk", "Asia/Karachi", + "pl", "Europe/Warsaw", + "pm", "America/Miquelon", + "pn", "Pacific/Pitcairn", + "pr", "America/Puerto_Rico", + "ps", "Asia/Gaza", + "pt", "Europe/Lisbon", + "pt", "Atlantic/Azores", + "pw", "Pacific/Palau", + "py", "America/Asuncion", + "qa", "Asia/Qatar", + "re", "Indian/Reunion", + "ro", "Europe/Bucharest", + "rs", "Europe/Belgrade", + "ru", "Asia/Kamchatka", + "ru", "Asia/Magadan", + "ru", "Asia/Vladivostok", + "ru", "Asia/Yakutsk", + "ru", "Asia/Irkutsk", + "ru", "Asia/Krasnoyarsk", + "ru", "Asia/Novosibirsk", + "ru", "Asia/Yekaterinburg", + "ru", "Europe/Samara", + "ru", "Europe/Moscow", + "ru", "Europe/Kaliningrad", + "rw", "Africa/Kigali", + "sa", "Asia/Riyadh", + "sb", "Pacific/Guadalcanal", + "sc", "Indian/Mahe", + "sd", "Africa/Khartoum", + "se", "Europe/Stockholm", + "sg", "Asia/Singapore", + "sh", "Atlantic/St_Helena", + "si", "Europe/Ljubljana", + "sj", "Arctic/Longyearbyen", + "sk", "Europe/Bratislava", + "sl", "Africa/Freetown", + "sm", "Europe/San_Marino", + "sn", "Africa/Dakar", + "so", "Africa/Mogadishu", + "sr", "America/Paramaribo", + "st", "Africa/Sao_Tome", + "sv", "America/El_Salvador", + "sy", "Asia/Damascus", + "sz", "Africa/Mbabane", + "tc", "America/Grand_Turk", + "td", "Africa/Ndjamena", + "tf", "Indian/Kerguelen", + "tg", "Africa/Lome", + "th", "Asia/Bangkok", + "tj", "Asia/Dushanbe", + "tk", "Pacific/Fakaofo", + "tl", "Asia/Dili", + "tm", "Asia/Ashgabat", + "tn", "Africa/Tunis", + "to", "Pacific/Tongatapu", + "tr", "Europe/Istanbul", + "tt", "America/Port_of_Spain", + "tv", "Pacific/Funafuti", + "tw", "Asia/Taipei", + "tz", "Africa/Dar_es_Salaam", + "ua", "Europe/Kiev", + "ug", "Africa/Kampala", + "um", "Pacific/Wake", + "um", "Pacific/Johnston", + "um", "Pacific/Midway", + "us", "America/New_York", + "us", "America/Chicago", + "us", "America/Denver", + "us", "America/Los_Angeles", + "us", "America/Anchorage", + "us", "Pacific/Honolulu", + "uy", "America/Montevideo", + "uz", "Asia/Tashkent", + "va", "Europe/Vatican", + "vc", "America/St_Vincent", + "ve", "America/Caracas", + "vg", "America/Tortola", + "vi", "America/St_Thomas", + "vn", "Asia/Saigon", + "vu", "Pacific/Efate", + "wf", "Pacific/Wallis", + "ws", "Pacific/Apia", + "ye", "Asia/Aden", + "yt", "Indian/Mayotte", + "za", "Africa/Johannesburg", + "zm", "Africa/Lusaka", + "zw", "Africa/Harare", + }; + + for (int i = 0; i < world.length; i += 2) { + String country = world[i]; + String name = world[i + 1]; + + TimeZone tz = TimeZone.getTimeZone(name); + Calendar c = Calendar.getInstance(tz); + TimeZone guess; + + c.set(2009, Calendar.JULY, 20, 12, 00, 00); + guess = guess(c, country); + assertEquals(name, guess.getID()); + + c.set(2009, Calendar.JANUARY, 20, 12, 00, 00); + guess = guess(c, country); + assertEquals(name, guess.getID()); + } + } + + public void testWorldWeird() throws Exception { + String[] world = new String[] { + // Distinguisable from Sydney only when DST not in effect + "au", "Australia/Lord_Howe", + }; + + for (int i = 0; i < world.length; i += 2) { + String country = world[i]; + String name = world[i + 1]; + + TimeZone tz = TimeZone.getTimeZone(name); + Calendar c = Calendar.getInstance(tz); + TimeZone guess; + + c.set(2009, Calendar.JULY, 20, 12, 00, 00); + guess = guess(c, country); + assertEquals(name, guess.getID()); + } + } + + private static TimeZone guess(Calendar c, String country) { + return TimeUtils.getTimeZone(c.get(c.ZONE_OFFSET) + c.get(c.DST_OFFSET), + c.get(c.DST_OFFSET) != 0, + c.getTimeInMillis(), + country); + } + + public void testFormatDuration() { + assertFormatDuration("0", 0); + assertFormatDuration("-1ms", -1); + assertFormatDuration("+1ms", 1); + assertFormatDuration("+10ms", 10); + assertFormatDuration("+100ms", 100); + assertFormatDuration("+101ms", 101); + assertFormatDuration("+330ms", 330); + assertFormatDuration("+1s330ms", 1330); + assertFormatDuration("+10s24ms", 10024); + } + + public void testFormatHugeDuration() { + //assertFormatDuration("+15542d1h11m11s555ms", 1342833071555L); + // TODO: improve formatDuration() API + assertFormatDuration("+999d23h59m59s999ms", 1342833071555L); + assertFormatDuration("-999d23h59m59s999ms", -1342833071555L); + } + + private void assertFormatDuration(String expected, long duration) { + StringBuilder sb = new StringBuilder(); + TimeUtils.formatDuration(duration, sb); + assertEquals("formatDuration(" + duration + ")", expected, sb.toString()); + } +} diff --git a/src/main/java/android/util/TimedRemoteCaller.java b/src/main/java/android/util/TimedRemoteCaller.java new file mode 100644 index 0000000..abb2b64 --- /dev/null +++ b/src/main/java/android/util/TimedRemoteCaller.java @@ -0,0 +1,134 @@ +/* + * 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 android.util; + +import android.os.SystemClock; + +import java.util.concurrent.TimeoutException; + +/** + * This is a helper class for making an async one way call and + * its async one way response response in a sync fashion within + * a timeout. The key idea is to call the remote method with a + * sequence number and a callback and then starting to wait for + * the response. The remote method calls back with the result and + * the sequence number. If the response comes within the timeout + * and its sequence number is the one sent in the method invocation, + * then the call succeeded. If the response does not come within + * the timeout then the call failed. Older result received when + * waiting for the result are ignored. + *

    + * Typical usage is: + *

    + *

    
    + * public class MyMethodCaller extends TimeoutRemoteCallHelper {
    + *     // The one way remote method to call.
    + *     private final IRemoteInterface mTarget;
    + *
    + *     // One way callback invoked when the remote method is done.
    + *     private final IRemoteCallback mCallback = new IRemoteCallback.Stub() {
    + *         public void onCompleted(Object result, int sequence) {
    + *             onRemoteMethodResult(result, sequence);
    + *         }
    + *     };
    + *
    + *     public MyMethodCaller(IRemoteInterface target) {
    + *         mTarget = target;
    + *     }
    + *
    + *     public Object onCallMyMethod(Object arg) throws RemoteException {
    + *         final int sequence = onBeforeRemoteCall();
    + *         mTarget.myMethod(arg, sequence);
    + *         return getResultTimed(sequence);
    + *     }
    + * }
    + * 

    + * + * @param The type of the expected result. + * + * @hide + */ +public abstract class TimedRemoteCaller { + + public static final long DEFAULT_CALL_TIMEOUT_MILLIS = 5000; + + private static final int UNDEFINED_SEQUENCE = -1; + + private final Object mLock = new Object(); + + private final long mCallTimeoutMillis; + + private int mSequenceCounter; + + private int mReceivedSequence = UNDEFINED_SEQUENCE; + + private int mAwaitedSequence = UNDEFINED_SEQUENCE; + + private T mResult; + + public TimedRemoteCaller(long callTimeoutMillis) { + mCallTimeoutMillis = callTimeoutMillis; + } + + public final int onBeforeRemoteCall() { + synchronized (mLock) { + mAwaitedSequence = mSequenceCounter++; + return mAwaitedSequence; + } + } + + public final T getResultTimed(int sequence) throws TimeoutException { + synchronized (mLock) { + final boolean success = waitForResultTimedLocked(sequence); + if (!success) { + throw new TimeoutException("No reponse for sequence: " + sequence); + } + T result = mResult; + mResult = null; + return result; + } + } + + public final void onRemoteMethodResult(T result, int sequence) { + synchronized (mLock) { + if (sequence == mAwaitedSequence) { + mReceivedSequence = sequence; + mResult = result; + mLock.notifyAll(); + } + } + } + + private boolean waitForResultTimedLocked(int sequence) { + final long startMillis = SystemClock.uptimeMillis(); + while (true) { + try { + if (mReceivedSequence == sequence) { + return true; + } + final long elapsedMillis = SystemClock.uptimeMillis() - startMillis; + final long waitMillis = mCallTimeoutMillis - elapsedMillis; + if (waitMillis <= 0) { + return false; + } + mLock.wait(waitMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } +} diff --git a/src/main/java/android/util/TimingLogger.java b/src/main/java/android/util/TimingLogger.java new file mode 100644 index 0000000..be442da --- /dev/null +++ b/src/main/java/android/util/TimingLogger.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2007 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 android.util; + +import java.util.ArrayList; + +import android.os.SystemClock; + +/** + * A utility class to help log timings splits throughout a method call. + * Typical usage is: + * + *
    + *     TimingLogger timings = new TimingLogger(TAG, "methodA");
    + *     // ... do some work A ...
    + *     timings.addSplit("work A");
    + *     // ... do some work B ...
    + *     timings.addSplit("work B");
    + *     // ... do some work C ...
    + *     timings.addSplit("work C");
    + *     timings.dumpToLog();
    + * 
    + * + *

    The dumpToLog call would add the following to the log:

    + * + *
    + *     D/TAG     ( 3459): methodA: begin
    + *     D/TAG     ( 3459): methodA:      9 ms, work A
    + *     D/TAG     ( 3459): methodA:      1 ms, work B
    + *     D/TAG     ( 3459): methodA:      6 ms, work C
    + *     D/TAG     ( 3459): methodA: end, 16 ms
    + * 
    + */ +public class TimingLogger { + + /** + * The Log tag to use for checking Log.isLoggable and for + * logging the timings. + */ + private String mTag; + + /** A label to be included in every log. */ + private String mLabel; + + /** Used to track whether Log.isLoggable was enabled at reset time. */ + private boolean mDisabled; + + /** Stores the time of each split. */ + ArrayList mSplits; + + /** Stores the labels for each split. */ + ArrayList mSplitLabels; + + /** + * Create and initialize a TimingLogger object that will log using + * the specific tag. If the Log.isLoggable is not enabled to at + * least the Log.VERBOSE level for that tag at creation time then + * the addSplit and dumpToLog call will do nothing. + * @param tag the log tag to use while logging the timings + * @param label a string to be displayed with each log + */ + public TimingLogger(String tag, String label) { + reset(tag, label); + } + + /** + * Clear and initialize a TimingLogger object that will log using + * the specific tag. If the Log.isLoggable is not enabled to at + * least the Log.VERBOSE level for that tag at creation time then + * the addSplit and dumpToLog call will do nothing. + * @param tag the log tag to use while logging the timings + * @param label a string to be displayed with each log + */ + public void reset(String tag, String label) { + mTag = tag; + mLabel = label; + reset(); + } + + /** + * Clear and initialize a TimingLogger object that will log using + * the tag and label that was specified previously, either via + * the constructor or a call to reset(tag, label). If the + * Log.isLoggable is not enabled to at least the Log.VERBOSE + * level for that tag at creation time then the addSplit and + * dumpToLog call will do nothing. + */ + public void reset() { + mDisabled = !Log.isLoggable(mTag, Log.VERBOSE); + if (mDisabled) return; + if (mSplits == null) { + mSplits = new ArrayList(); + mSplitLabels = new ArrayList(); + } else { + mSplits.clear(); + mSplitLabels.clear(); + } + addSplit(null); + } + + /** + * Add a split for the current time, labeled with splitLabel. If + * Log.isLoggable was not enabled to at least the Log.VERBOSE for + * the specified tag at construction or reset() time then this + * call does nothing. + * @param splitLabel a label to associate with this split. + */ + public void addSplit(String splitLabel) { + if (mDisabled) return; + long now = SystemClock.elapsedRealtime(); + mSplits.add(now); + mSplitLabels.add(splitLabel); + } + + /** + * Dumps the timings to the log using Log.d(). If Log.isLoggable was + * not enabled to at least the Log.VERBOSE for the specified tag at + * construction or reset() time then this call does nothing. + */ + public void dumpToLog() { + if (mDisabled) return; + Log.d(mTag, mLabel + ": begin"); + final long first = mSplits.get(0); + long now = first; + for (int i = 1; i < mSplits.size(); i++) { + now = mSplits.get(i); + final String splitLabel = mSplitLabels.get(i); + final long prev = mSplits.get(i - 1); + + Log.d(mTag, mLabel + ": " + (now - prev) + " ms, " + splitLabel); + } + Log.d(mTag, mLabel + ": end, " + (now - first) + " ms"); + } +} diff --git a/src/main/java/android/util/TouchModeFlexibleAsserts.java b/src/main/java/android/util/TouchModeFlexibleAsserts.java new file mode 100644 index 0000000..ca12a15 --- /dev/null +++ b/src/main/java/android/util/TouchModeFlexibleAsserts.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2008 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 android.util; + +import junit.framework.Assert; + +import android.test.InstrumentationTestCase; +import android.test.TouchUtils; +import android.view.View; + +/** + * When entering touch mode via touch, the tests can be flaky. These asserts + * are more flexible (allowing up to MAX_ATTEMPTS touches to enter touch mode via touch or + * tap) until we can find a way to solve the flakiness. + */ +public class TouchModeFlexibleAsserts { + + private static int MAX_ATTEMPTS = 2; + + private static int MAX_DELAY_MILLIS = 2000; + + public static void assertInTouchModeAfterClick(InstrumentationTestCase test, View viewToTouch) { + int numAttemptsAtTouchMode = 0; + while (numAttemptsAtTouchMode < MAX_ATTEMPTS && + !viewToTouch.isInTouchMode()) { + TouchUtils.clickView(test, viewToTouch); + numAttemptsAtTouchMode++; + } + Assert.assertTrue("even after " + MAX_ATTEMPTS + " clicks, did not enter " + + "touch mode", viewToTouch.isInTouchMode()); + //Assert.assertEquals("number of touches to enter touch mode", 1, numAttemptsAtTouchMode); + } + + public static void assertInTouchModeAfterTap(InstrumentationTestCase test, View viewToTouch) { + int numAttemptsAtTouchMode = 0; + while (numAttemptsAtTouchMode < MAX_ATTEMPTS && + !viewToTouch.isInTouchMode()) { + TouchUtils.tapView(test, viewToTouch); + numAttemptsAtTouchMode++; + } + Assert.assertTrue("even after " + MAX_ATTEMPTS + " taps, did not enter " + + "touch mode", viewToTouch.isInTouchMode()); + //Assert.assertEquals("number of touches to enter touch mode", 1, numAttemptsAtTouchMode); + } + + public static void assertNotInTouchModeAfterKey(InstrumentationTestCase test, int keyCode, View checkForTouchMode) { + test.sendKeys(keyCode); + int amountLeft = MAX_DELAY_MILLIS; + + while (checkForTouchMode.isInTouchMode() && amountLeft > 0) { + amountLeft -= 200; + try { + Thread.sleep(200); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + Assert.assertFalse("even after waiting " + MAX_DELAY_MILLIS + " millis after " + + "pressing key event, still in touch mode", checkForTouchMode.isInTouchMode()); + } +} diff --git a/src/main/java/android/util/TrustedTime.java b/src/main/java/android/util/TrustedTime.java new file mode 100644 index 0000000..263d782 --- /dev/null +++ b/src/main/java/android/util/TrustedTime.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2011 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 android.util; + +/** + * Interface that provides trusted time information, possibly coming from an NTP + * server. Implementations may cache answers until {@link #forceRefresh()}. + * + * @hide + */ +public interface TrustedTime { + /** + * Force update with an external trusted time source, returning {@code true} + * when successful. + */ + public boolean forceRefresh(); + + /** + * Check if this instance has cached a response from a trusted time source. + */ + public boolean hasCache(); + + /** + * Return time since last trusted time source contact, or + * {@link Long#MAX_VALUE} if never contacted. + */ + public long getCacheAge(); + + /** + * Return certainty of cached trusted time in milliseconds, or + * {@link Long#MAX_VALUE} if never contacted. Smaller values are more + * precise. + */ + public long getCacheCertainty(); + + /** + * Return current time similar to {@link System#currentTimeMillis()}, + * possibly using a cached authoritative time source. + */ + public long currentTimeMillis(); +} diff --git a/src/main/java/android/util/TypedValue.java b/src/main/java/android/util/TypedValue.java new file mode 100644 index 0000000..74d4245 --- /dev/null +++ b/src/main/java/android/util/TypedValue.java @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2007 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 android.util; + +/** + * Container for a dynamically typed data value. Primarily used with + * {@link android.content.res.Resources} for holding resource values. + */ +public class TypedValue { + /** The value contains no data. */ + public static final int TYPE_NULL = 0x00; + + /** The data field holds a resource identifier. */ + public static final int TYPE_REFERENCE = 0x01; + /** The data field holds an attribute resource + * identifier (referencing an attribute in the current theme + * style, not a resource entry). */ + public static final int TYPE_ATTRIBUTE = 0x02; + /** The string field holds string data. In addition, if + * data is non-zero then it is the string block + * index of the string and assetCookie is the set of + * assets the string came from. */ + public static final int TYPE_STRING = 0x03; + /** The data field holds an IEEE 754 floating point number. */ + public static final int TYPE_FLOAT = 0x04; + /** The data field holds a complex number encoding a + * dimension value. */ + public static final int TYPE_DIMENSION = 0x05; + /** The data field holds a complex number encoding a fraction + * of a container. */ + public static final int TYPE_FRACTION = 0x06; + + /** Identifies the start of plain integer values. Any type value + * from this to {@link #TYPE_LAST_INT} means the + * data field holds a generic integer value. */ + public static final int TYPE_FIRST_INT = 0x10; + + /** The data field holds a number that was + * originally specified in decimal. */ + public static final int TYPE_INT_DEC = 0x10; + /** The data field holds a number that was + * originally specified in hexadecimal (0xn). */ + public static final int TYPE_INT_HEX = 0x11; + /** The data field holds 0 or 1 that was originally + * specified as "false" or "true". */ + public static final int TYPE_INT_BOOLEAN = 0x12; + + /** Identifies the start of integer values that were specified as + * color constants (starting with '#'). */ + public static final int TYPE_FIRST_COLOR_INT = 0x1c; + + /** The data field holds a color that was originally + * specified as #aarrggbb. */ + public static final int TYPE_INT_COLOR_ARGB8 = 0x1c; + /** The data field holds a color that was originally + * specified as #rrggbb. */ + public static final int TYPE_INT_COLOR_RGB8 = 0x1d; + /** The data field holds a color that was originally + * specified as #argb. */ + public static final int TYPE_INT_COLOR_ARGB4 = 0x1e; + /** The data field holds a color that was originally + * specified as #rgb. */ + public static final int TYPE_INT_COLOR_RGB4 = 0x1f; + + /** Identifies the end of integer values that were specified as color + * constants. */ + public static final int TYPE_LAST_COLOR_INT = 0x1f; + + /** Identifies the end of plain integer values. */ + public static final int TYPE_LAST_INT = 0x1f; + + /* ------------------------------------------------------------ */ + + /** Complex data: bit location of unit information. */ + public static final int COMPLEX_UNIT_SHIFT = 0; + /** Complex data: mask to extract unit information (after shifting by + * {@link #COMPLEX_UNIT_SHIFT}). This gives us 16 possible types, as + * defined below. */ + public static final int COMPLEX_UNIT_MASK = 0xf; + + /** {@link #TYPE_DIMENSION} complex unit: Value is raw pixels. */ + public static final int COMPLEX_UNIT_PX = 0; + /** {@link #TYPE_DIMENSION} complex unit: Value is Device Independent + * Pixels. */ + public static final int COMPLEX_UNIT_DIP = 1; + /** {@link #TYPE_DIMENSION} complex unit: Value is a scaled pixel. */ + public static final int COMPLEX_UNIT_SP = 2; + /** {@link #TYPE_DIMENSION} complex unit: Value is in points. */ + public static final int COMPLEX_UNIT_PT = 3; + /** {@link #TYPE_DIMENSION} complex unit: Value is in inches. */ + public static final int COMPLEX_UNIT_IN = 4; + /** {@link #TYPE_DIMENSION} complex unit: Value is in millimeters. */ + public static final int COMPLEX_UNIT_MM = 5; + + /** {@link #TYPE_FRACTION} complex unit: A basic fraction of the overall + * size. */ + public static final int COMPLEX_UNIT_FRACTION = 0; + /** {@link #TYPE_FRACTION} complex unit: A fraction of the parent size. */ + public static final int COMPLEX_UNIT_FRACTION_PARENT = 1; + + /** Complex data: where the radix information is, telling where the decimal + * place appears in the mantissa. */ + public static final int COMPLEX_RADIX_SHIFT = 4; + /** Complex data: mask to extract radix information (after shifting by + * {@link #COMPLEX_RADIX_SHIFT}). This give us 4 possible fixed point + * representations as defined below. */ + public static final int COMPLEX_RADIX_MASK = 0x3; + + /** Complex data: the mantissa is an integral number -- i.e., 0xnnnnnn.0 */ + public static final int COMPLEX_RADIX_23p0 = 0; + /** Complex data: the mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn */ + public static final int COMPLEX_RADIX_16p7 = 1; + /** Complex data: the mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn */ + public static final int COMPLEX_RADIX_8p15 = 2; + /** Complex data: the mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn */ + public static final int COMPLEX_RADIX_0p23 = 3; + + /** Complex data: bit location of mantissa information. */ + public static final int COMPLEX_MANTISSA_SHIFT = 8; + /** Complex data: mask to extract mantissa information (after shifting by + * {@link #COMPLEX_MANTISSA_SHIFT}). This gives us 23 bits of precision; + * the top bit is the sign. */ + public static final int COMPLEX_MANTISSA_MASK = 0xffffff; + + /* ------------------------------------------------------------ */ + + /** + * {@link #TYPE_NULL} data indicating the value was not specified. + */ + public static final int DATA_NULL_UNDEFINED = 0; + /** + * {@link #TYPE_NULL} data indicating the value was explicitly set to null. + */ + public static final int DATA_NULL_EMPTY = 1; + + /* ------------------------------------------------------------ */ + + /** + * If {@link #density} is equal to this value, then the density should be + * treated as the system's default density value: {@link DisplayMetrics#DENSITY_DEFAULT}. + */ + public static final int DENSITY_DEFAULT = 0; + + /** + * If {@link #density} is equal to this value, then there is no density + * associated with the resource and it should not be scaled. + */ + public static final int DENSITY_NONE = 0xffff; + + /* ------------------------------------------------------------ */ + + /** The type held by this value, as defined by the constants here. + * This tells you how to interpret the other fields in the object. */ + public int type; + + /** If the value holds a string, this is it. */ + public CharSequence string; + + /** Basic data in the value, interpreted according to {@link #type} */ + public int data; + + /** Additional information about where the value came from; only + * set for strings. */ + public int assetCookie; + + /** If Value came from a resource, this holds the corresponding resource id. */ + public int resourceId; + + /** If Value came from a resource, these are the configurations for which + * its contents can change. */ + public int changingConfigurations = -1; + + /** + * If the Value came from a resource, this holds the corresponding pixel density. + * */ + public int density; + + /* ------------------------------------------------------------ */ + + /** Return the data for this value as a float. Only use for values + * whose type is {@link #TYPE_FLOAT}. */ + public final float getFloat() { + return Float.intBitsToFloat(data); + } + + private static final float MANTISSA_MULT = + 1.0f / (1<>TypedValue.COMPLEX_RADIX_SHIFT) + & TypedValue.COMPLEX_RADIX_MASK]; + } + + /** + * Converts a complex data value holding a dimension to its final floating + * point value. The given data must be structured as a + * {@link #TYPE_DIMENSION}. + * + * @param data A complex data value holding a unit, magnitude, and + * mantissa. + * @param metrics Current display metrics to use in the conversion -- + * supplies display density and scaling information. + * + * @return The complex floating point value multiplied by the appropriate + * metrics depending on its unit. + */ + public static float complexToDimension(int data, DisplayMetrics metrics) + { + return applyDimension( + (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK, + complexToFloat(data), + metrics); + } + + /** + * Converts a complex data value holding a dimension to its final value + * as an integer pixel offset. This is the same as + * {@link #complexToDimension}, except the raw floating point value is + * truncated to an integer (pixel) value. + * The given data must be structured as a + * {@link #TYPE_DIMENSION}. + * + * @param data A complex data value holding a unit, magnitude, and + * mantissa. + * @param metrics Current display metrics to use in the conversion -- + * supplies display density and scaling information. + * + * @return The number of pixels specified by the data and its desired + * multiplier and units. + */ + public static int complexToDimensionPixelOffset(int data, + DisplayMetrics metrics) + { + return (int)applyDimension( + (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK, + complexToFloat(data), + metrics); + } + + /** + * Converts a complex data value holding a dimension to its final value + * as an integer pixel size. This is the same as + * {@link #complexToDimension}, except the raw floating point value is + * converted to an integer (pixel) value for use as a size. A size + * conversion involves rounding the base value, and ensuring that a + * non-zero base value is at least one pixel in size. + * The given data must be structured as a + * {@link #TYPE_DIMENSION}. + * + * @param data A complex data value holding a unit, magnitude, and + * mantissa. + * @param metrics Current display metrics to use in the conversion -- + * supplies display density and scaling information. + * + * @return The number of pixels specified by the data and its desired + * multiplier and units. + */ + public static int complexToDimensionPixelSize(int data, + DisplayMetrics metrics) + { + final float value = complexToFloat(data); + final float f = applyDimension( + (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK, + value, + metrics); + final int res = (int)(f+0.5f); + if (res != 0) return res; + if (value == 0) return 0; + if (value > 0) return 1; + return -1; + } + + /** + * @hide Was accidentally exposed in API level 1 for debugging purposes. + * Kept for compatibility just in case although the debugging code has been removed. + */ + @Deprecated + public static float complexToDimensionNoisy(int data, DisplayMetrics metrics) + { + return complexToDimension(data, metrics); + } + + /** + * Return the complex unit type for this value. For example, a dimen type + * with value 12sp will return {@link #COMPLEX_UNIT_SP}. Only use for values + * whose type is {@link #TYPE_DIMENSION}. + * + * @return The complex unit type. + */ + public int getComplexUnit() + { + return COMPLEX_UNIT_MASK & (data>>TypedValue.COMPLEX_UNIT_SHIFT); + } + + /** + * Converts an unpacked complex data value holding a dimension to its final floating + * point value. The two parameters unit and value + * are as in {@link #TYPE_DIMENSION}. + * + * @param unit The unit to convert from. + * @param value The value to apply the unit to. + * @param metrics Current display metrics to use in the conversion -- + * supplies display density and scaling information. + * + * @return The complex floating point value multiplied by the appropriate + * metrics depending on its unit. + */ + public static float applyDimension(int unit, float value, + DisplayMetrics metrics) + { + switch (unit) { + case COMPLEX_UNIT_PX: + return value; + case COMPLEX_UNIT_DIP: + return value * metrics.density; + case COMPLEX_UNIT_SP: + return value * metrics.scaledDensity; + case COMPLEX_UNIT_PT: + return value * metrics.xdpi * (1.0f/72); + case COMPLEX_UNIT_IN: + return value * metrics.xdpi; + case COMPLEX_UNIT_MM: + return value * metrics.xdpi * (1.0f/25.4f); + } + return 0; + } + + /** + * Return the data for this value as a dimension. Only use for values + * whose type is {@link #TYPE_DIMENSION}. + * + * @param metrics Current display metrics to use in the conversion -- + * supplies display density and scaling information. + * + * @return The complex floating point value multiplied by the appropriate + * metrics depending on its unit. + */ + public float getDimension(DisplayMetrics metrics) + { + return complexToDimension(data, metrics); + } + + /** + * Converts a complex data value holding a fraction to its final floating + * point value. The given data must be structured as a + * {@link #TYPE_FRACTION}. + * + * @param data A complex data value holding a unit, magnitude, and + * mantissa. + * @param base The base value of this fraction. In other words, a + * standard fraction is multiplied by this value. + * @param pbase The parent base value of this fraction. In other + * words, a parent fraction (nn%p) is multiplied by this + * value. + * + * @return The complex floating point value multiplied by the appropriate + * base value depending on its unit. + */ + public static float complexToFraction(int data, float base, float pbase) + { + switch ((data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK) { + case COMPLEX_UNIT_FRACTION: + return complexToFloat(data) * base; + case COMPLEX_UNIT_FRACTION_PARENT: + return complexToFloat(data) * pbase; + } + return 0; + } + + /** + * Return the data for this value as a fraction. Only use for values whose + * type is {@link #TYPE_FRACTION}. + * + * @param base The base value of this fraction. In other words, a + * standard fraction is multiplied by this value. + * @param pbase The parent base value of this fraction. In other + * words, a parent fraction (nn%p) is multiplied by this + * value. + * + * @return The complex floating point value multiplied by the appropriate + * base value depending on its unit. + */ + public float getFraction(float base, float pbase) + { + return complexToFraction(data, base, pbase); + } + + /** + * Regardless of the actual type of the value, try to convert it to a + * string value. For example, a color type will be converted to a + * string of the form #aarrggbb. + * + * @return CharSequence The coerced string value. If the value is + * null or the type is not known, null is returned. + */ + public final CharSequence coerceToString() + { + int t = type; + if (t == TYPE_STRING) { + return string; + } + return coerceToString(t, data); + } + + private static final String[] DIMENSION_UNIT_STRS = new String[] { + "px", "dip", "sp", "pt", "in", "mm" + }; + private static final String[] FRACTION_UNIT_STRS = new String[] { + "%", "%p" + }; + + /** + * Perform type conversion as per {@link #coerceToString()} on an + * explicitly supplied type and data. + * + * @param type The data type identifier. + * @param data The data value. + * + * @return String The coerced string value. If the value is + * null or the type is not known, null is returned. + */ + public static final String coerceToString(int type, int data) + { + switch (type) { + case TYPE_NULL: + return null; + case TYPE_REFERENCE: + return "@" + data; + case TYPE_ATTRIBUTE: + return "?" + data; + case TYPE_FLOAT: + return Float.toString(Float.intBitsToFloat(data)); + case TYPE_DIMENSION: + return Float.toString(complexToFloat(data)) + DIMENSION_UNIT_STRS[ + (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK]; + case TYPE_FRACTION: + return Float.toString(complexToFloat(data)*100) + FRACTION_UNIT_STRS[ + (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK]; + case TYPE_INT_HEX: + return "0x" + Integer.toHexString(data); + case TYPE_INT_BOOLEAN: + return data != 0 ? "true" : "false"; + } + + if (type >= TYPE_FIRST_COLOR_INT && type <= TYPE_LAST_COLOR_INT) { + return "#" + Integer.toHexString(data); + } else if (type >= TYPE_FIRST_INT && type <= TYPE_LAST_INT) { + return Integer.toString(data); + } + + return null; + } + + public void setTo(TypedValue other) + { + type = other.type; + string = other.string; + data = other.data; + assetCookie = other.assetCookie; + resourceId = other.resourceId; + density = other.density; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("TypedValue{t=0x").append(Integer.toHexString(type)); + sb.append("/d=0x").append(Integer.toHexString(data)); + if (type == TYPE_STRING) { + sb.append(" \"").append(string != null ? string : "").append("\""); + } + if (assetCookie != 0) { + sb.append(" a=").append(assetCookie); + } + if (resourceId != 0) { + sb.append(" r=0x").append(Integer.toHexString(resourceId)); + } + sb.append("}"); + return sb.toString(); + } +}; + diff --git a/src/main/java/android/util/Xml.java b/src/main/java/android/util/Xml.java new file mode 100644 index 0000000..041e8a8 --- /dev/null +++ b/src/main/java/android/util/Xml.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2007 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 android.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import org.apache.harmony.xml.ExpatReader; +import org.kxml2.io.KXmlParser; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; +import org.xmlpull.v1.XmlSerializer; + +/** + * XML utility methods. + */ +public class Xml { + /** @hide */ public Xml() {} + + /** + * {@link org.xmlpull.v1.XmlPullParser} "relaxed" feature name. + * + * @see + * specification + */ + public static String FEATURE_RELAXED = "http://xmlpull.org/v1/doc/features.html#relaxed"; + + /** + * Parses the given xml string and fires events on the given SAX handler. + */ + public static void parse(String xml, ContentHandler contentHandler) + throws SAXException { + try { + XMLReader reader = new ExpatReader(); + reader.setContentHandler(contentHandler); + reader.parse(new InputSource(new StringReader(xml))); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + /** + * Parses xml from the given reader and fires events on the given SAX + * handler. + */ + public static void parse(Reader in, ContentHandler contentHandler) + throws IOException, SAXException { + XMLReader reader = new ExpatReader(); + reader.setContentHandler(contentHandler); + reader.parse(new InputSource(in)); + } + + /** + * Parses xml from the given input stream and fires events on the given SAX + * handler. + */ + public static void parse(InputStream in, Encoding encoding, + ContentHandler contentHandler) throws IOException, SAXException { + XMLReader reader = new ExpatReader(); + reader.setContentHandler(contentHandler); + InputSource source = new InputSource(in); + source.setEncoding(encoding.expatName); + reader.parse(source); + } + + /** + * Returns a new pull parser with namespace support. + */ + public static XmlPullParser newPullParser() { + try { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + return parser; + } catch (XmlPullParserException e) { + throw new AssertionError(); + } + } + + /** + * Creates a new xml serializer. + */ + public static XmlSerializer newSerializer() { + try { + return XmlSerializerFactory.instance.newSerializer(); + } catch (XmlPullParserException e) { + throw new AssertionError(e); + } + } + + /** Factory for xml serializers. Initialized on demand. */ + static class XmlSerializerFactory { + static final String TYPE + = "org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer"; + static final XmlPullParserFactory instance; + static { + try { + instance = XmlPullParserFactory.newInstance(TYPE, null); + } catch (XmlPullParserException e) { + throw new AssertionError(e); + } + } + } + + /** + * Supported character encodings. + */ + public enum Encoding { + + US_ASCII("US-ASCII"), + UTF_8("UTF-8"), + UTF_16("UTF-16"), + ISO_8859_1("ISO-8859-1"); + + final String expatName; + + Encoding(String expatName) { + this.expatName = expatName; + } + } + + /** + * Finds an encoding by name. Returns UTF-8 if you pass {@code null}. + */ + public static Encoding findEncodingByName(String encodingName) + throws UnsupportedEncodingException { + if (encodingName == null) { + return Encoding.UTF_8; + } + + for (Encoding encoding : Encoding.values()) { + if (encoding.expatName.equalsIgnoreCase(encodingName)) + return encoding; + } + throw new UnsupportedEncodingException(encodingName); + } + + /** + * Return an AttributeSet interface for use with the given XmlPullParser. + * If the given parser itself implements AttributeSet, that implementation + * is simply returned. Otherwise a wrapper class is + * instantiated on top of the XmlPullParser, as a proxy for retrieving its + * attributes, and returned to you. + * + * @param parser The existing parser for which you would like an + * AttributeSet. + * + * @return An AttributeSet you can use to retrieve the + * attribute values at each of the tags as the parser moves + * through its XML document. + * + * @see AttributeSet + */ + public static AttributeSet asAttributeSet(XmlPullParser parser) { + return (parser instanceof AttributeSet) + ? (AttributeSet) parser + : new XmlPullAttributes(parser); + } +} diff --git a/src/main/java/android/util/XmlPullAttributes.java b/src/main/java/android/util/XmlPullAttributes.java new file mode 100644 index 0000000..6c8bb39 --- /dev/null +++ b/src/main/java/android/util/XmlPullAttributes.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2006 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 android.util; + +import org.xmlpull.v1.XmlPullParser; + +import android.util.AttributeSet; + +import com.android.internal.util.XmlUtils; + +/** + * Provides an implementation of AttributeSet on top of an XmlPullParser. + */ +class XmlPullAttributes implements AttributeSet { + public XmlPullAttributes(XmlPullParser parser) { + mParser = parser; + } + + public int getAttributeCount() { + return mParser.getAttributeCount(); + } + + public String getAttributeName(int index) { + return mParser.getAttributeName(index); + } + + public String getAttributeValue(int index) { + return mParser.getAttributeValue(index); + } + + public String getAttributeValue(String namespace, String name) { + return mParser.getAttributeValue(namespace, name); + } + + public String getPositionDescription() { + return mParser.getPositionDescription(); + } + + public int getAttributeNameResource(int index) { + return 0; + } + + public int getAttributeListValue(String namespace, String attribute, + String[] options, int defaultValue) { + return XmlUtils.convertValueToList( + getAttributeValue(namespace, attribute), options, defaultValue); + } + + public boolean getAttributeBooleanValue(String namespace, String attribute, + boolean defaultValue) { + return XmlUtils.convertValueToBoolean( + getAttributeValue(namespace, attribute), defaultValue); + } + + public int getAttributeResourceValue(String namespace, String attribute, + int defaultValue) { + return XmlUtils.convertValueToInt( + getAttributeValue(namespace, attribute), defaultValue); + } + + public int getAttributeIntValue(String namespace, String attribute, + int defaultValue) { + return XmlUtils.convertValueToInt( + getAttributeValue(namespace, attribute), defaultValue); + } + + public int getAttributeUnsignedIntValue(String namespace, String attribute, + int defaultValue) { + return XmlUtils.convertValueToUnsignedInt( + getAttributeValue(namespace, attribute), defaultValue); + } + + public float getAttributeFloatValue(String namespace, String attribute, + float defaultValue) { + String s = getAttributeValue(namespace, attribute); + if (s != null) { + return Float.parseFloat(s); + } + return defaultValue; + } + + public int getAttributeListValue(int index, + String[] options, int defaultValue) { + return XmlUtils.convertValueToList( + getAttributeValue(index), options, defaultValue); + } + + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + return XmlUtils.convertValueToBoolean( + getAttributeValue(index), defaultValue); + } + + public int getAttributeResourceValue(int index, int defaultValue) { + return XmlUtils.convertValueToInt( + getAttributeValue(index), defaultValue); + } + + public int getAttributeIntValue(int index, int defaultValue) { + return XmlUtils.convertValueToInt( + getAttributeValue(index), defaultValue); + } + + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + return XmlUtils.convertValueToUnsignedInt( + getAttributeValue(index), defaultValue); + } + + public float getAttributeFloatValue(int index, float defaultValue) { + String s = getAttributeValue(index); + if (s != null) { + return Float.parseFloat(s); + } + return defaultValue; + } + + public String getIdAttribute() { + return getAttributeValue(null, "id"); + } + + public String getClassAttribute() { + return getAttributeValue(null, "class"); + } + + public int getIdAttributeResourceValue(int defaultValue) { + return getAttributeResourceValue(null, "id", defaultValue); + } + + public int getStyleAttribute() { + return getAttributeResourceValue(null, "style", 0); + } + + /*package*/ XmlPullParser mParser; +} diff --git a/src/main/java/android/util/Xml_Delegate.java b/src/main/java/android/util/Xml_Delegate.java new file mode 100644 index 0000000..a193330 --- /dev/null +++ b/src/main/java/android/util/Xml_Delegate.java @@ -0,0 +1,49 @@ +/* + * 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 android.util; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Delegate overriding some methods of android.util.Xml + * + * Through the layoutlib_create tool, the original methods of Xml have been replaced + * by calls to methods of the same name in this delegate class. + * + * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager} + * around to map int to instance of the delegate. + */ +public class Xml_Delegate { + + @LayoutlibDelegate + /*package*/ static XmlPullParser newPullParser() { + try { + KXmlParser parser = new KXmlParser(); + // The prebuilt kxml2 library with the IDE doesn't support DOCECL. +// parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + return parser; + } catch (XmlPullParserException e) { + throw new AssertionError(); + } + } +} diff --git a/src/main/java/framework/AI/AStar.java b/src/main/java/framework/AI/AStar.java new file mode 100644 index 0000000..a5324f0 --- /dev/null +++ b/src/main/java/framework/AI/AStar.java @@ -0,0 +1,94 @@ +package framework.AI; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; + +/** + * A*クラス。 + */ +public class AStar { + private LinkedList locationOpenList = new LinkedList(); + private LinkedList locationClosedList = new LinkedList(); + + /** + * 最短経路探査。 最短経路のリストを返すか、見つからない場合nullを返す。 + * start = goalの場合もnullを返す。 + * 戻り値にはgoalからstartの1個前まで経路をさかのぼったものが入っている。startは入っていない。 + */ + public Plan getPath(Location startLocation, + Location goalLocation) { + + //出発地点と目的地点が同じならnullを返す + if (startLocation.equals(goalLocation)) { + return null; + } + + // 初期化 + locationOpenList = new LinkedList(); + locationClosedList = new LinkedList(); + + // 出発地点をLocationOpenListに追加する + locationOpenList.add(startLocation); + while (!locationOpenList.isEmpty()) { + // LocationOpenListの最小スコアの地点をcurLocationとして取り出す + Collections.sort(locationOpenList, new CostComparator()); + Location curLocation = locationOpenList.removeFirst(); + + // 現在の地点が目的地点ならば探査完了 + if (curLocation.equals(goalLocation)) { + LinkedList locationPath = new LinkedList(); + + // 出発地点まで通った地点を辿る + curLocation = goalLocation; + while (!curLocation.equals(startLocation)) { + locationPath.add(curLocation); + curLocation = (Location) curLocation.getParent(); + } + return new Plan(locationPath); + } + + // 現在の地点が目的地点でないなら、現在の地点をLocationClosedListに移す + locationClosedList.add(curLocation); + + // 隣接する地点を調べる + ArrayList neighbors = curLocation.getSuccessors(); + + // 隣接する地点の数だけ、ループ + for (int i = 0; i < neighbors.size(); i++) { + // 通過でき、各リストに入ってないならコストをもらい、 + if (!locationOpenList.contains(neighbors.get(i)) + && !locationClosedList.contains(neighbors.get(i))) { + //地点コストを求める + //agent.getLocationCost((Location)neighbors.get(i), null, null, null); + + //パスコストを求める + ((Location)neighbors.get(i)).calculatesCosts(curLocation, goalLocation); + + // LocationOpenListに移す + locationOpenList.add((Location) neighbors.get(i)); + } + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + + } + return null; + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + // コストの昇順ソートのためのクラスを作ろう + + /** + * ソート条件式クラス。 + */ + class CostComparator implements Comparator { + public int compare(Location n1, Location n2) { + return n1.getScore() - n2.getScore(); + } + } + + // ------------------------------------------------------------------------------------------------------------------------------------------- + +} diff --git a/src/main/java/framework/AI/GeometryGraph.java b/src/main/java/framework/AI/GeometryGraph.java new file mode 100644 index 0000000..9ebb8b3 --- /dev/null +++ b/src/main/java/framework/AI/GeometryGraph.java @@ -0,0 +1,168 @@ +package framework.AI; + +import framework.model3D.Position3D; + +import javax.media.j3d.Geometry; +import javax.media.j3d.IndexedTriangleArray; +import javax.vecmath.Point3d; +import java.util.ArrayList; + +public class GeometryGraph extends StateMachine { + // コンストラクタ + public GeometryGraph(Geometry g) { + if (g instanceof IndexedTriangleArray) { + + IndexedTriangleArray ita = (IndexedTriangleArray) g; + + // itaからstatesにデータを書き込み + for (int i = 0; i < ita.getIndexCount() / 3; i++) {// 面の数だけループ + Location e = new Location(i, ita); + if (e.getNormal() != null) { + states.add(e); + } + } + + // 関連付け + for (int i = 0; i < states.size(); i++) { + setSuccessors(i, ita); + } + } + } + + + // 連結成分を関連付け + public void setSuccessors(int index, IndexedTriangleArray ita) { + Location e = (Location)states.get(index); + + // 探索 + int[] List = new int[ita.getIndexCount()]; + + for (int i = 0; i < ita.getIndexCount(); i++) { + List[i] = ita.getCoordinateIndex(i); + } + + // -1で初期化 + for (int i = 0; i < 3; i++) { + e.successorIndex[i] = -1; + e.IndexOfSharedEdge[i] = -1; + } + + int j = 0; + + for (int i = 0; i < states.size() * 3; i++) { + // System.out.println("indexList[0] とList["+i+"] の照合"); + // //////idでListを置き換えてメソッド呼び出しを少なく? + // int id = ita.getCoordinateIndex(i); + if (e.indexList[0] == List[i]) { + if (i % 3 == 0) { + if (e.indexList[1] == List[i + 2]) { + e.successorIndex[j] = i / 3; + e.IndexOfSharedEdge[0] = e.indexList[0]; + e.IndexOfSharedEdge[1] = e.indexList[1]; + e.addSuccessor(states.get(i / 3)); + j++; + } else if (e.indexList[2] == List[i + 1]) { + e.successorIndex[j] = i / 3; + e.IndexOfSharedEdge[0] = e.indexList[0]; + e.IndexOfSharedEdge[2] = e.indexList[2]; + e.addSuccessor(states.get(i / 3)); + j++; + } + } else if (i % 3 == 1) { + if (e.indexList[1] == List[i - 1]) { + e.successorIndex[j] = i / 3; + e.IndexOfSharedEdge[0] = e.indexList[0]; + e.IndexOfSharedEdge[1] = e.indexList[1]; + e.addSuccessor(states.get(i / 3)); + j++; + } else if (e.indexList[2] == List[i + 1]) { + e.successorIndex[j] = i / 3; + e.IndexOfSharedEdge[0] = e.indexList[0]; + e.IndexOfSharedEdge[2] = e.indexList[2]; + e.addSuccessor(states.get(i / 3)); + j++; + } + } else if (i % 3 == 2) { + if (e.indexList[1] == List[i - 1]) { + e.successorIndex[j] = i / 3; + e.IndexOfSharedEdge[0] = e.indexList[0]; + e.IndexOfSharedEdge[1] = e.indexList[1]; + e.addSuccessor(states.get(i / 3)); + j++; + } else if (e.indexList[2] == List[i - 2]) { + e.successorIndex[j] = i / 3; + e.IndexOfSharedEdge[0] = e.indexList[0]; + e.IndexOfSharedEdge[2] = e.indexList[2]; + e.addSuccessor(states.get(i / 3)); + j++; + } + } + + } + } + + for (int i = 0; i < states.size() * 3; i++) { + // System.out.println("indexList[1] とList["+i+"] の照合"); + if (e.indexList[1] == List[i]) { + if (i % 3 == 0) { + if (e.indexList[2] == List[i + 2]) { + e.successorIndex[j] = i / 3; + e.IndexOfSharedEdge[1] = e.indexList[1]; + e.IndexOfSharedEdge[2] = e.indexList[2]; + e.addSuccessor(states.get(i / 3)); + j++; + } + } else if (i % 3 == 1) { + if (e.indexList[2] == List[i - 1]) { + e.successorIndex[j] = i / 3; + e.IndexOfSharedEdge[1] = e.indexList[1]; + e.IndexOfSharedEdge[2] = e.indexList[2]; + e.addSuccessor(states.get(i / 3)); + j++; + } + } else if (i % 3 == 2) { + if (e.indexList[2] == List[i - 2]) { + e.successorIndex[j] = i / 3; + e.IndexOfSharedEdge[1] = e.indexList[1]; + e.IndexOfSharedEdge[2] = e.indexList[2]; + e.addSuccessor(states.get(i / 3)); + j++; + } + } + } + } + + // System.out.println("1つ目のsuccessor**"+successorIndex[0]+"***"); + // System.out.println("2つ目のsuccessor**"+successorIndex[1]+"***"); + // System.out.println("3つ目のsuccessor**"+successorIndex[2]+"***"); + + return; + } + + + // アクセッサ + public ArrayList getStates() { + return states; + } + + public Location getNearestLocation(Position3D pos) { + Location nearest = null; + double distance = 0.0; + for (int n = 0; n < states.size(); n++) { + Location loc = (Location)states.get(n); + if (nearest == null) { + nearest = loc; + distance = nearest.getCenter().distance( + new Point3d(pos.getVector3d())); + } else { + double d = loc.getCenter().distance( + new Point3d(pos.getVector3d())); + if (d < distance) { + nearest = loc; + distance = d; + } + } + } + return nearest; + } +} diff --git a/src/main/java/framework/AI/IState.java b/src/main/java/framework/AI/IState.java new file mode 100644 index 0000000..31ecfdf --- /dev/null +++ b/src/main/java/framework/AI/IState.java @@ -0,0 +1,16 @@ +package framework.AI; + +import java.util.ArrayList; + +public interface IState { + + abstract ArrayList getSuccessors(); + + abstract void addSuccessor(IState s); + /** + * 探査元のノード取得。 + * @return + * 探査元のノードを返す。 + */ + abstract IState getParent(); +} diff --git a/src/main/java/framework/AI/Location.java b/src/main/java/framework/AI/Location.java new file mode 100644 index 0000000..a1a4e2b --- /dev/null +++ b/src/main/java/framework/AI/Location.java @@ -0,0 +1,137 @@ +package framework.AI; + +import framework.model3D.GeometryUtility; + +import javax.media.j3d.IndexedTriangleArray; +import javax.vecmath.Point3d; +import javax.vecmath.Vector3d; +import java.util.ArrayList; + +public class Location implements IState { + public int planeIndex; + public int[] indexList = new int[3]; + public int[] successorIndex = new int[100]; + public int numberOfSharedEdge;// 共有辺の本数 + public int[] IndexOfSharedEdge = new int[3];// 共有辺の頂点インデックス + private Point3d center; + private Vector3d normal; + + private double cost = 0; + private double heuristicCost = 0; + private Location parentNode = null; + private ArrayList successors = new ArrayList(); + + // テスト用の空のコンストラクタ + public Location(Point3d center) { + this.center = center; + normal = new Vector3d(0.0, 1.0, 0.0); + } + + // コンストラクタ + public Location(int index, IndexedTriangleArray ita) { + planeIndex = index; + + indexList[0] = ita.getCoordinateIndex(index * 3); + indexList[1] = ita.getCoordinateIndex(index * 3 + 1); + indexList[2] = ita.getCoordinateIndex(index * 3 + 2); + + Point3d p1 = new Point3d(); + Point3d p2 = new Point3d(); + Point3d p3 = new Point3d(); + + ita.getCoordinate(indexList[0], p1); + ita.getCoordinate(indexList[1], p2); + ita.getCoordinate(indexList[2], p3); + + // 中心座標の計算 + center = new Point3d((p1.getX() + p2.getX() + p3.getX()) / 3.0, (p1 + .getY() + + p2.getY() + p3.getY()) / 3.0, (p1.getZ() + p2.getZ() + p3 + .getZ()) / 3.0); + + // 法線ベクトルの計算 + p2.sub(p1); + p3.sub(p1); + Vector3d v2 = new Vector3d(p2); + Vector3d v3 = new Vector3d(p3); + v2.cross(v2, v3); + if (v2.length() < GeometryUtility.TOLERANCE) { + v2 = null; + } else { + v2.normalize(); + } + normal = v2; + } + + public void addSuccessor(IState s) { + successors.add(s); + } + + @Override + public ArrayList getSuccessors() { + // TODO Auto-generated method stub + return (ArrayList) successors; + } + + @Override + public IState getParent() { + // TODO Auto-generated method stub + return parentNode; + } + + /** + * スコアの取得。 + * + * @return スコアを返す。 + */ + public int getScore() { + // 比較に小数点も反映させる為、1000を掛けている + return (int) ((cost + heuristicCost) * 1000); + } + + /** + * コスト計算と探査元ノードを設定。 + * + * @param parentNode + * 探査元のノード。 + * @param goalNode + * 目的地のノード。 + */ + public void calculatesCosts(Location parentNode, Location goalNode) { + // コストを加算 + cost = parentNode.cost + 1; // agent.getPathCost(parentNode, this, null, + // null, null, null, null, null); + + // //ヒューリスティックコストを計算 + // disX = point.x - goalNode.point.x; + // disY = point.y - goalNode.point.y; + // ヒューリスティックコストの信頼性が薄い為、3分の1にしている + heuristicCost = 0; + + // 探査元ノードを記録 + this.parentNode = parentNode; + } + + // ///////// + // アクセッサ// + // ///////// + public int getPlaneIndex() { + return planeIndex; + } + + public int getIndexList(int index) { + return indexList[index]; + } + + public int getSuccessorIndex(int index) { + return successorIndex[index]; + } + + public Point3d getCenter() { + return center; + } + + public Vector3d getNormal() { + return normal; + } +} diff --git a/src/main/java/framework/AI/Plan.java b/src/main/java/framework/AI/Plan.java new file mode 100644 index 0000000..05cf5be --- /dev/null +++ b/src/main/java/framework/AI/Plan.java @@ -0,0 +1,73 @@ +package framework.AI; + +import framework.model3D.Position3D; + +import javax.vecmath.Vector3d; +import java.util.LinkedList; + +public class Plan { + private LinkedList path; // 計画内容を表すパス + private int currentLoc = 0; // パス上の現在の Location + + /** + * 複数の通過点を持つ計画 + * @param locationPath + */ + public Plan(LinkedList locationPath) { + path = locationPath; + currentLoc = locationPath.size() - 1; + } + + /** + * スタートとゴールをダイレクトに結ぶ計画 + * @param start + * @param goal + */ + public Plan(Location start, Location goal) { + path = new LinkedList(); + path.add(goal); + path.add(start); + currentLoc = 1; + } + + /** + * 現在の Location を取得する + * @return 現在の Location, すでにゴールに着いているときは null を返す + */ + public Location getCurrentLocation() { + if (currentLoc <= 0) return null; + return path.get(currentLoc); + } + + /** + * 次の Location を取得する + * @return 次の Location, すでにゴールに着いているときは null を返す + */ + public Location getNextLocation() { + if (currentLoc <= 0) return null; + return path.get(currentLoc - 1); + } + + /** + * 現在の座標値を元に、現在の Location を更新する + * @param position 現在の座標値 + * @return 更新した --- true, 以前のまま --- false + */ + public boolean updateCurrentLocation(Position3D position) { + Vector3d toCurrentPosition = position.getVector3d(); + Location curLocation = getCurrentLocation(); + if (curLocation == null) return true; + toCurrentPosition.sub(curLocation.getCenter()); + double distanceToCurrentPosition = toCurrentPosition.length(); + Vector3d toNextLocation = new Vector3d(getNextLocation().getCenter()); + toNextLocation.sub(curLocation.getCenter()); + double distanceToNextLocation = toNextLocation.length(); + if (distanceToCurrentPosition >= distanceToNextLocation) { + // 次の Location を通り過ぎた場合 + currentLoc--; + return true; + } + return false; + } + +} diff --git a/src/main/java/framework/AI/StateMachine.java b/src/main/java/framework/AI/StateMachine.java new file mode 100644 index 0000000..73e82d9 --- /dev/null +++ b/src/main/java/framework/AI/StateMachine.java @@ -0,0 +1,9 @@ +package framework.AI; + +import java.util.ArrayList; + +public abstract class StateMachine { + protected ArrayList states = new ArrayList(); + protected IState curState; + +} diff --git a/src/main/java/framework/RWT/DefaultSelector.java b/src/main/java/framework/RWT/DefaultSelector.java new file mode 100644 index 0000000..70e0aa7 --- /dev/null +++ b/src/main/java/framework/RWT/DefaultSelector.java @@ -0,0 +1,12 @@ +package framework.RWT; + +import java.awt.*; + + +public class DefaultSelector extends RWTSelector { + @Override + public void paint(Graphics g) { + g.setColor(new Color(0.0f, 0.5f, 1.0f, 0.3f)); + g.fill3DRect(widget.getAbsoluteX() - 5, widget.getAbsoluteY() - 5, widget.getAbsoluteWidth() + 10, widget.getAbsoluteHeight() + 10, true); + } +} diff --git a/src/main/java/framework/RWT/RWTBoard.java b/src/main/java/framework/RWT/RWTBoard.java new file mode 100644 index 0000000..d30a69c --- /dev/null +++ b/src/main/java/framework/RWT/RWTBoard.java @@ -0,0 +1,60 @@ +package framework.RWT; + +import java.awt.*; + +public class RWTBoard extends RWTWidget { + protected float relativeX = 0.0f; + protected float relativeY = 0.0f; + protected float relativeWidth = 0.0f; + protected float relativeHeight = 0.0f; + private int x = 0; + private int y = 0; + private int width = 0; + private int height = 0; + + public RWTBoard(float x, float y, float w, float h, Color c) { + setRelativePosition(x, y); + setRelativeSize(w, h); + setColor(c); + } + + /** + * 相対位置を指定する。 + * @param x x座標値(0.0f〜1.0f) + * @param y y座標値(0.0f〜1.0f) + */ + public void setRelativePosition(float x, float y) { + relativeX = x; + relativeY = y; + } + /** + * 相対サイズを決定する。 + * @param w 幅(0.0f〜1.0f) + * @param h 高さ(0.0f〜1.0f) + */ + public void setRelativeSize(float w, float h) { + relativeWidth = w; + relativeHeight = h; + } + + @Override + public void adjust(Component parent) { + int sx = parent.getWidth(); + int sy = parent.getHeight(); + x = (int) (sx * relativeX); + y = (int) (sy * relativeY); + width = (int) (sx * relativeWidth); + height = (int) (sy * relativeHeight); + } + + @Override + public void paint(Graphics g) { + g.setColor(new Color(0.0f, 0.0f, 0.0f, 0.3f)); + g.fillRoundRect(x + 5, y + 5, width, height, 10, 10); + g.setColor(color); + g.fillRoundRect(x, y, width, height, 10, 10); + g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue())); + g.drawRoundRect(x, y, width, height, 10, 10); + } + +} diff --git a/src/main/java/framework/RWT/RWTButton.java b/src/main/java/framework/RWT/RWTButton.java new file mode 100644 index 0000000..fc29d58 --- /dev/null +++ b/src/main/java/framework/RWT/RWTButton.java @@ -0,0 +1,34 @@ +package framework.RWT; + +import java.awt.*; + +/** + * ボタンです。 + * @author Wataru + * + */ +public abstract class RWTButton extends RWTLabel implements RWTSelectableWidget { + private float relativeX = 0.0f; + private float relativeY = 0.0f; + private float relativeWidth = 0.0f; + private float relativeHeight = 0.0f; + private int x = 0; + private int y = 0; + private int width = 0; + private int height = 0; + + @Override + public void adjust(Component parent) { + int sx = parent.getWidth(); + int sy = parent.getHeight(); + x = (int) (sx * relativeX); + y = (int) (sy * relativeY); + width = (int) (sx * relativeWidth); + height = (int) (sy * relativeHeight); + } + + @Override + public void paint(Graphics g) { + // 未実装 + } +} diff --git a/src/main/java/framework/RWT/RWTCanvas3D.java b/src/main/java/framework/RWT/RWTCanvas3D.java new file mode 100644 index 0000000..620450f --- /dev/null +++ b/src/main/java/framework/RWT/RWTCanvas3D.java @@ -0,0 +1,230 @@ +package framework.RWT; + +import com.sun.j3d.utils.universe.SimpleUniverse; +import framework.model3D.IViewer3D; +import framework.schedule.ScheduleManager; +import framework.schedule.TaskController; +import framework.view3D.Camera3D; +import framework.view3D.Viewer3D; + +import javax.media.j3d.Canvas3D; +import javax.media.j3d.ImageComponent2D; +import javax.media.j3d.View; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.ArrayList; + +/** + * GUI部品(RWTWidget)を配置可能なCanvas3D、親コンテナに対して相対座標で配置できる + * @author 新田直也 + * + */ +@SuppressWarnings("serial") +public class RWTCanvas3D extends Canvas3D { + private ArrayList widgetList = new ArrayList(); + private float relativeX = 0.0f; + private float relativeY = 0.0f; + private float relativeWidth = 0.0f; + private float relativeHeight = 0.0f; + private int x = 0; + private int y = 0; + private int width = 0; + private int height = 0; + + private Camera3D camera = null; + private IViewer3D viewer = null; + + private DisplayCanvas displayCanvas = null; + private BufferedImage image = null; + private static TaskController renderingController = ScheduleManager.getInstance().registerTask("rendering"); + + public RWTCanvas3D() { + this(SimpleUniverse.getPreferredConfiguration()); + } + + public RWTCanvas3D(GraphicsConfiguration gc) { + this(gc, false); + } + + public RWTCanvas3D(boolean offScreen) { + this(SimpleUniverse.getPreferredConfiguration(), offScreen); + } + + public RWTCanvas3D(GraphicsConfiguration gc, boolean offScreen) { + super(gc, offScreen); + setFocusable(false); + if (offScreen) { + displayCanvas = new DisplayCanvas(); + } else { + enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); + } + renderingController.deactivate(); + } + + public Canvas getDisplayCanvas() { + if (isOffScreen()) { + return displayCanvas; + } + return this; + } + + public void attachCamera(Camera3D camera) { + viewer = new Viewer3D(camera); + View oldView = this.getView(); + if (oldView != null) oldView.removeCanvas3D(this); + camera.getView().addCanvas3D(this); + this.camera = camera; + } + + /** + * Widgetを配置する。 + * @param widget + */ + public void addWidget(RWTWidget widget) { + widgetList.add(widget); + } + + /** + * RWTCanvas3Dの相対位置を決定する。 + * @param x x座標値(0.0f〜1.0f) + * @param y y座標値(0.0f〜1.0f) + */ + public void setRelativePosition(float x, float y) { + relativeX = x; + relativeY = y; + } + + /** + * RWTCanvas3Dの相対サイズを決定する。 + * @param w 幅(0.0f〜1.0f) + * @param h 高さ(0.0f〜1.0f) + */ + public void setRelativeSize(float w, float h) { + relativeWidth = w; + relativeHeight = h; + } + + /** + * 親のコンテナに応じて位置とサイズを合わせる。 + * @param parent 親コンテナ + */ + public void adjust(Container parent) { + int sx = parent.getWidth(); + int sy = parent.getHeight(); + x = (int) (sx * relativeX); + y = (int) (sy * relativeY); + width = (int) (sx * relativeWidth); + height = (int) (sy * relativeHeight); + if (isOffScreen()) { + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + ImageComponent2D ic = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, image, true, false); + ic.setCapability(ImageComponent2D.ALLOW_IMAGE_READ); + ic.setCapability(ImageComponent2D.ALLOW_IMAGE_WRITE); + setOffScreenBuffer(ic); + displayCanvas.setBounds(x, y, width, height); + getScreen3D().setSize(width, height); + getScreen3D().setPhysicalScreenWidth(0.0254/90.0 * (double)width); + getScreen3D().setPhysicalScreenHeight(0.0254/90.0 * (double)height); + } else { + setBounds(x, y, width, height); + } + } + + @Override + public void preRender() { + if (viewer == null || camera == null) return; + viewer.setGraphicsContext3D(this.getGraphicsContext3D()); + camera.getUniverse().preRender(viewer); + } + + @Override + public void renderField(int fieldDesc) { + if (viewer == null || camera == null) return; + viewer.setGraphicsContext3D(this.getGraphicsContext3D()); + camera.getUniverse().renderField(viewer); + } + + @Override + public void postRender() { + if (viewer == null || camera == null) return; + viewer.setGraphicsContext3D(this.getGraphicsContext3D()); + camera.getUniverse().postRender(viewer); + if (isOffScreen()) { + // オフスクリーンバッファ上の画像取得 + ImageComponent2D ic = getOffScreenBuffer(); + image = ic.getImage(); + Graphics src = image.getGraphics(); + // キャンバス上の Widget を合成 + drawWidgets(src); + src.dispose(); + // ディスプレイバッファに描画 + Graphics dst = displayCanvas.getGraphics(); + dst.drawImage(image, 0, 0, null); + dst.dispose(); + } + } + + @Override + public void postSwap() { + super.postSwap(); + + if (!isOffScreen()) { + // キャンバス上の Widget の描画 + Graphics g = getGraphics(); + drawWidgets(g); + g.dispose(); + } + renderingController.deactivate(); + } + + /** + * オフスクリーンの場合のみ、レンダリングを明示的にスケジューリングする + */ + public void doRender() { + if (isOffScreen()) { + if (renderingController.activate()) { + // 前回のレンダリングが終了していた場合 + renderOffScreenBuffer(); + } + } + } + + private void drawWidgets(Graphics g) { + for(int i = 0; i < widgetList.size(); i++) { + RWTWidget widget = widgetList.get(i); + if(widget.isVisible()) { + widget.adjust(this); + widget.paint(g); + } + } + } + + public void processEvent(AWTEvent e) { + // イベントをコンテナ側に返す + Container c = this.getParent(); + if (c != null) c.dispatchEvent(e); + } + + private class DisplayCanvas extends Canvas { + + public DisplayCanvas() { + setFocusable(false); + enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); + } + + public void paint(Graphics g) { + super.paint(g); + if (image != null) { + synchronized (image) { + g.drawImage(image, 0, 0, null); + } + } + } + + public void processEvent(AWTEvent e) { + // イベントをコンテナ側に返す + Container c = this.getParent(); + if (c != null) c.dispatchEvent(e); + } + } +} diff --git a/src/main/java/framework/RWT/RWTContainer.java b/src/main/java/framework/RWT/RWTContainer.java new file mode 100644 index 0000000..b8c96f7 --- /dev/null +++ b/src/main/java/framework/RWT/RWTContainer.java @@ -0,0 +1,171 @@ +package framework.RWT; + +import java.awt.*; +import java.util.ArrayList; + + +/** + * GUI部品(RWTWidget)を配置可能なコンテナ、GUI部品のアクティベーションも管理できる + * + * @author 新田直也 + * + */ +@SuppressWarnings("serial") +public abstract class RWTContainer extends Container { + private ArrayList canvases = null; + + private ArrayList widgetList = new ArrayList(); + private RWTSelectionManager active = new RWTSelectionManager(); + + public abstract void build(GraphicsConfiguration gc); + + public abstract void keyPressed(RWTVirtualKey key); + + public abstract void keyReleased(RWTVirtualKey key); + + public abstract void keyTyped(RWTVirtualKey key); + + public RWTContainer() { + setLayout(null); + setFocusable(false); + widgetList.add(active.getSelector()); + enableEvents(AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); + } + + public void addCanvas(RWTCanvas3D cavas) { + if (canvases == null) canvases = new ArrayList(); + canvases.add(cavas); + cavas.adjust(this); + add(cavas.getDisplayCanvas()); + } + + public void paint(Graphics g) { + super.paint(g); + + if (canvases != null) { + for (int i = 0; i < canvases.size(); i++) { + canvases.get(i).adjust(this); + canvases.get(i).getDisplayCanvas().paint(g); + } + } + + for (int i = 0; i < widgetList.size(); i++) { + RWTWidget widget = widgetList.get(i); + if (widget.isVisible()) { + widget.adjust(this); + widget.paint(g); + } + } + } + + public RWTCanvas3D getPrimaryRWTCanvas3D() { + if (canvases == null) return null; + return canvases.get(0); + } + + public int getNumberOfRWTCanvas3D() { + if (canvases == null) return 0; + return canvases.size(); + } + + public RWTCanvas3D getRWTCanvas3D(int index) { + return canvases.get(index); + } + + /** + * カーソルで選べないWidgetを加えます。 + * + * @param widget + */ + public void addWidget(RWTWidget widget) { + widgetList.add(widget); + } + + /** + * カーソルで選べないWidgetを加えます。 + * + * @param widget + */ + public void addWidgetOnBack(RWTWidget widget) { + widgetList.add(0, widget); + } + + /** + *  アクティベーションを操作するマネージャーを設定する。 + * + * @param manager + */ + public void setActiveManager(RWTSelectionManager manager) { + active = manager; + } + + /** + * カーソルで選べるWidgetを加えます。 + * + * @param w + * @param n + * @param m + */ + public void addSelectableWidget(RWTSelectableWidget r, int n, int m) { + active.add(r, n, m); + widgetList.add(0, (RWTWidget) r); + } + + /** + * カーソルで選べるWidgetを前面に加えます。 + * + * @param w + * @param n + * @param m + */ + public void addSelectableWidgetOnFront(RWTSelectableWidget r, int n, int m) { + active.add(r, n, m); + widgetList.add((RWTWidget) r); + } + + /** + * カーソルをひとつ上に動かします。 + */ + public void cursorMoveUp() { + active.up(); + repaint(); + } + + /** + * カーソルをひとつ下に動かします。 + */ + public void cursorMoveDown() { + active.down(); + repaint(); + } + + /** + * カーソルをひとつ左に動かします。 + */ + public void cursorMoveLeft() { + active.left(); + repaint(); + } + + /** + * カーソルをひとつ右に動かします。 + */ + public void cursorMoveRight() { + active.right(); + repaint(); + } + + public RWTWidget getSelectedWidget() { + return active.getSelectedWidget(); + } + + public void processEvent(AWTEvent e) { + // イベントを RWTFrame3D に返す + Container c = this.getParent(); + while (c != null && !(c instanceof RWTFrame3D)) { + c = c.getParent(); + } + if (c != null) ((RWTFrame3D)c).processEvent(e); + super.processEvent(e); + } +} diff --git a/src/main/java/framework/RWT/RWTFrame3D.java b/src/main/java/framework/RWT/RWTFrame3D.java new file mode 100644 index 0000000..07f3c56 --- /dev/null +++ b/src/main/java/framework/RWT/RWTFrame3D.java @@ -0,0 +1,166 @@ +package framework.RWT; + +import javax.swing.*; +import javax.swing.event.MouseInputListener; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; + + +public class RWTFrame3D extends JFrame implements KeyListener, MouseInputListener { + + private static final long serialVersionUID = 1L; + private RWTVirtualController virtualController = new RWTVirtualController(); + private RWTContainer container; + private Robot robot = null; + private boolean bShadowCasting = false; + private boolean bMouseCapture = false; + + public RWTFrame3D() { + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); +// GraphicsDevice screen = GraphicsEnvironment. +// getLocalGraphicsEnvironment().getDefaultScreenDevice(); +// setUndecorated(true); +// screen.setFullScreenWindow(this); +// screen.setDisplayMode(new DisplayMode(800, 600, 32, +// DisplayMode.REFRESH_RATE_UNKNOWN)); + addKeyListener(this); + addMouseListener(this); + addMouseMotionListener(this); + try { + robot = new Robot(); + } catch (AWTException e) { + } + requestFocus(); + } + + public void setContentPane(RWTContainer contentPane) { + container = contentPane; + super.setContentPane(contentPane); + setVisible(true); + contentPane.repaint(); + requestFocus(); + } + + /** + * レンダリングの開始(オフスクリーンモードの場合のみ有効) + */ + public void doRender() { + if (container == null) return; + for (int n = 0; n < container.getNumberOfRWTCanvas3D(); n++) { + container.getRWTCanvas3D(n).doRender(); + } + } + + public void keyPressed(KeyEvent e) { + // TODO Auto-generated method stub + if (0 <= e.getKeyCode() && e.getKeyCode() < 256) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) System.exit(0); + virtualController.setKeyDown(e.getKeyCode(), true); + if (virtualController.getVirtualKey(e.getKeyCode()) != null) { + if (container != null) container.keyPressed(virtualController.getVirtualKey(e.getKeyCode())); + } + } + } + + public void keyReleased(KeyEvent e) { + // TODO Auto-generated method stub + if (0 <= e.getKeyCode() && e.getKeyCode() < 256) { + virtualController.setKeyDown(e.getKeyCode(), false); + if (virtualController.getVirtualKey(e.getKeyCode()) != null) { + if (container != null)container.keyReleased(virtualController.getVirtualKey(e.getKeyCode())); + } + } + } + + public void keyTyped(KeyEvent e) { + // TODO Auto-generated method stub + } + + public RWTVirtualController getVirtualController() { + return virtualController; + } + + public void setShadowCasting(boolean b) { + bShadowCasting = b; + } + + public boolean isShadowCasting() { + return bShadowCasting; + } + + public void setMouseCapture(boolean b) { + bMouseCapture = b; + } + + public void processEvent(AWTEvent e) { + super.processEvent(e); + } + + @Override + public void mouseClicked(MouseEvent e) { + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + @Override + public void mousePressed(MouseEvent e) { + switch (e.getButton()) { + case MouseEvent.BUTTON1: + virtualController.setMouseButtonDown(0, true); + break; + case MouseEvent.BUTTON2: + virtualController.setMouseButtonDown(1, true); + break; + case MouseEvent.BUTTON3: + virtualController.setMouseButtonDown(2, true); + break; + } + } + + @Override + public void mouseReleased(MouseEvent e) { + switch (e.getButton()) { + case MouseEvent.BUTTON1: + virtualController.setMouseButtonDown(0, false); + break; + case MouseEvent.BUTTON2: + virtualController.setMouseButtonDown(1, false); + break; + case MouseEvent.BUTTON3: + virtualController.setMouseButtonDown(2, false); + break; + } + } + + @Override + public void mouseDragged(MouseEvent e) { + if (bMouseCapture ) { + int cx = getWidth() / 2; + int cy = getHeight() / 2; + robot.mouseMove(cx, cy); + virtualController.moveMousePosition((double)(e.getXOnScreen()- cx) / (double)getWidth(), (double)(e.getYOnScreen() - cy) / (double)getHeight()); + } else { + virtualController.setMousePosition((double)e.getXOnScreen() / (double)getWidth(), (double)e.getYOnScreen() / (double)getHeight()); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + if (bMouseCapture) { + int cx = getWidth() / 2; + int cy = getHeight() / 2; + robot.mouseMove(cx, cy); + virtualController.moveMousePosition((double)(e.getXOnScreen() - cx) / (double)getWidth(), (double)(e.getYOnScreen() - cy) / (double)getHeight()); + } else { + virtualController.setMousePosition((double)e.getXOnScreen() / (double)getWidth(), (double)e.getYOnScreen() / (double)getHeight()); + } + } +} diff --git a/src/main/java/framework/RWT/RWTImage.java b/src/main/java/framework/RWT/RWTImage.java new file mode 100644 index 0000000..e86194d --- /dev/null +++ b/src/main/java/framework/RWT/RWTImage.java @@ -0,0 +1,91 @@ +package framework.RWT; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; +import java.io.File; + +/** + * 画像です。 + * @author Wataru + * + */ +public class RWTImage extends RWTWidget { + protected float relativeX = 0.0f; + protected float relativeY = 0.0f; + protected float relativeWidth = 0.0f; + protected float relativeHeight = 0.0f; + protected int x = 0; + protected int y = 0; + protected int width = 0; + protected int height = 0; + + private BufferedImage image = null; + private ImageObserver observer; + + private int imageWidth = 0; + private int imageHeight = 0; + + public RWTImage(String fileName) { + try { + image = ImageIO.read(new File(fileName)); + setSize(image.getWidth(), image.getHeight()); + } catch (Exception e) { + e.printStackTrace(); + image = null; + } + } + + /** + * 画像の相対位置を指定する。 + * @param x x座標値(0.0f〜1.0f) + * @param y y座標値(0.0f〜1.0f) + */ + public void setRelativePosition(float x, float y) { + relativeX = x; + relativeY = y; + } + + /** + * 画像を設定します。 + * @param fileName + */ + public void setImage(String fileName) { + try { + image = ImageIO.read(new File(fileName)); + setSize(image.getWidth(), image.getHeight()); + } catch (Exception e) { + e.printStackTrace(); + image = null; + } + } + + /** + * 画像のサイズを設定します。 + * 相対値ではありません。 + * @param x + * @param y + */ + public void setSize(int x, int y) { + imageWidth = image.getWidth(); + imageHeight = image.getHeight(); + } + + public void setObserver(ImageObserver o) { + observer = o; + } + + @Override + public void adjust(Component parent) { + int sx = parent.getWidth(); + int sy = parent.getHeight(); + x = (int) (sx * relativeX); + y = (int) (sy * relativeY); + } + + @Override + public void paint(Graphics g) { + g.drawImage(image, x, y, imageWidth, imageHeight, observer); + } +} diff --git a/src/main/java/framework/RWT/RWTImageButton.java b/src/main/java/framework/RWT/RWTImageButton.java new file mode 100644 index 0000000..90e31bb --- /dev/null +++ b/src/main/java/framework/RWT/RWTImageButton.java @@ -0,0 +1,44 @@ +package framework.RWT; +/** + * 画像のボタンです。 + * @author Wataru + * + */ +public class RWTImageButton extends RWTImage implements RWTSelectableWidget { + + public RWTImageButton(String fileName) { + super(fileName); + } + + public void selected() { + } + + public void buttonDown() { + } + + public void buttonUp() { + } + + public void deselected() { + } + + @Override + public int getAbsoluteHeight() { + return height; + } + + @Override + public int getAbsoluteWidth() { + return width; + } + + @Override + public int getAbsoluteX() { + return x; + } + + @Override + public int getAbsoluteY() { + return y; + } +} diff --git a/src/main/java/framework/RWT/RWTLabel.java b/src/main/java/framework/RWT/RWTLabel.java new file mode 100644 index 0000000..9110d45 --- /dev/null +++ b/src/main/java/framework/RWT/RWTLabel.java @@ -0,0 +1,141 @@ +package framework.RWT; + +import java.awt.*; + +/** + * 文字列を表示するものです。 + * @author Wataru + * + */ +public class RWTLabel extends RWTWidget { + private float relativeBaselineX = 0.0f; + private float relativeBaselineY = 0.0f; + private float relativeWidth = 0.0f; + private float relativeHeight = 0.0f; + private Font relativeFont = new Font("", Font.PLAIN, 12); + + private int baselineX = 0; + private int baselineY = 0; + private int y = 0; + private int width = 0; + private int height = 0; + protected Font font = new Font("", Font.PLAIN, 12); + + protected String[] strings; + protected int drawMode = DRAW_RIGHT; + + public static final int DEFAULT_PARENT_WIDTH_IN_POINT = 500; + + public static final int DRAW_CENTER = 0; + public static final int DRAW_RIGHT = 1; + public static final int DRAW_LEFT = 2; + + public static final String NEW_PARAGRAPH = "#"; + + public RWTLabel() { + strings = new String[1]; + strings[0] = ""; + } + + public RWTLabel(float x, float y, String s, Color c, Font f) { + setRelativePosition(x, y); + setString(s); + setColor(c); + setFont(f); + } + + /** + * 相対位置を指定する。 + * @param x x座標値(0.0f〜1.0f) + * @param y y座標値(0.0f〜1.0f) + */ + public void setRelativePosition(float x, float y) { + relativeBaselineX = x; + relativeBaselineY = y; + } + + /** + * 相対位置を指定する。 + * @param x x座標値(0.0f〜1.0f) + * @param y y座標値(0.0f〜1.0f) + * @param m 表示方法 + */ + public void setRelativePosition(float x, float y, int m) { + setRelativePosition(x, y); + drawMode = m; + } + + /** + * 表示する文字列を設定する。 + * @param s + */ + public void setString(String s) { + if(s != null) { + strings = s.split(NEW_PARAGRAPH); + } + } + + /** + * 表示する文字列のフォントを設定する。ただし、フォントのサイズはキャンバスの幅が500ptであるときのものとする。 + * @param f + */ + public void setFont(Font f) { + relativeFont = f; + } + + public int getAbsoluteHeight() { + return height; + } + + public int getAbsoluteWidth() { + return width; + } + + public int getAbsoluteX() { + return baselineX; + } + + public int getAbsoluteY() { + return y; + } + + @Override + public void adjust(Component parent) { + int sx = parent.getWidth(); + int sy = parent.getHeight(); + baselineX = (int) (sx * relativeBaselineX); + baselineY = (int) (sy * relativeBaselineY); + font = new Font(relativeFont.getName(), + relativeFont.getStyle(), + (int)(relativeFont.getSize() * sx / DEFAULT_PARENT_WIDTH_IN_POINT)); + + FontMetrics fm = parent.getFontMetrics(font); + y = baselineY - fm.getAscent() + (int)(fm.getDescent() * 1.1); + width = fm.stringWidth(strings[0]); // 一番上の行の幅(※本当は最大幅にすべき) + height = fm.getAscent(); + } + + @Override + public void paint(Graphics g) { + FontMetrics fm = g.getFontMetrics(font); + g.setColor(color); + g.setFont(font); + int height = fm.getHeight(); + int h = 0; + for(int i = 0; i < strings.length; i++){ + int top = 0; + if(drawMode == DRAW_CENTER) { + top = fm.stringWidth(strings[i]) / 2; + } + else if(drawMode == DRAW_LEFT) { + top = fm.stringWidth(strings[i]); + } + + if (strings[i] != null && strings[i].length() > 0) { + g.drawString(strings[i], baselineX - top, baselineY + h); + } + + h += height; + } + } +} diff --git a/src/main/java/framework/RWT/RWTLine.java b/src/main/java/framework/RWT/RWTLine.java new file mode 100644 index 0000000..39ca764 --- /dev/null +++ b/src/main/java/framework/RWT/RWTLine.java @@ -0,0 +1,55 @@ +package framework.RWT; + +import java.awt.*; + +/** + * ラインです。 + * @author Wataru + * + */ +public class RWTLine extends RWTWidget { + private float relativeX1 = 0.0f; + private float relativeY1 = 0.0f; + private float relativeX2 = 0.0f; + private float relativeY2 = 0.0f; + private int x1 = 0; + private int y1 = 0; + private int x2 = 0; + private int y2 = 0; + + /** + * 場所を設定します。値は相対値です。 + * @param w + * @param h + */ + public void setRelativePosition(float x1, float y1, float x2, float y2) { + relativeX1 = x1; + relativeY1 = y1; + relativeX2 = x2; + relativeY2 = y2; + } + + @Override + public void adjust(Component parent) { + int sx = parent.getWidth(); + int sy = parent.getHeight(); + x1 = (int) (sx * relativeX1); + y1 = (int) (sy * relativeY1); + x2 = (int) (sx * relativeX2); + y2 = (int) (sy * relativeY2); + } + + @Override + public void paint(Graphics g) { + g.setColor(color); + g.drawLine(x1, y1, x2, y2); + } + +// @Override +// public void paint(Graphics g, ArrayList list, double scale) { +// // TODO Auto-generated method stub +// g.setColor(color); +// g.drawLine(list.get(0).x, list.get(0).y, list.get(1).x, list.get(1).y); +// } + +} diff --git a/src/main/java/framework/RWT/RWTSelectableWidget.java b/src/main/java/framework/RWT/RWTSelectableWidget.java new file mode 100644 index 0000000..2cc8f24 --- /dev/null +++ b/src/main/java/framework/RWT/RWTSelectableWidget.java @@ -0,0 +1,21 @@ +package framework.RWT; + +/** + * アクティブになるために必要なものです。 + * @author Wataru + * + */ +public interface RWTSelectableWidget { + + public abstract void selected(); + + public abstract void deselected(); + + public abstract int getAbsoluteX(); + + public abstract int getAbsoluteY(); + + public abstract int getAbsoluteWidth(); + + public abstract int getAbsoluteHeight(); +} diff --git a/src/main/java/framework/RWT/RWTSelectionCanvas3D.java b/src/main/java/framework/RWT/RWTSelectionCanvas3D.java new file mode 100644 index 0000000..9950a64 --- /dev/null +++ b/src/main/java/framework/RWT/RWTSelectionCanvas3D.java @@ -0,0 +1,68 @@ +package framework.RWT; + +import framework.model3D.BaseObject3D; +import framework.model3D.Position3D; +import framework.model3D.Universe; +import framework.view3D.Camera3D; + +import javax.media.j3d.AmbientLight; +import javax.media.j3d.BoundingSphere; +import javax.media.j3d.BranchGroup; +import javax.media.j3d.DirectionalLight; +import javax.vecmath.Color3f; +import javax.vecmath.Point3d; +import javax.vecmath.Vector3f; + +public class RWTSelectionCanvas3D extends RWTCanvas3D { + Universe universe = null; + BranchGroup objRoot = null; + + public RWTSelectionCanvas3D() { + this(0.0, 0.0, 12.0); + } + + public RWTSelectionCanvas3D(double cameraX, double cameraY, double cameraZ) { + universe = new Universe(); + objRoot = new BranchGroup(); + objRoot.setCapability(BranchGroup.ALLOW_DETACH); + objRoot.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND); + objRoot.setCapability(BranchGroup.ALLOW_CHILDREN_READ); + objRoot.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE); + universe.getRoot().addChild(objRoot); + createLight(universe); + setCamera(universe, cameraX, cameraY, cameraZ); + universe.compile(); + } + + private void setCamera(Universe universe, double cameraX, double cameraY, double cameraZ) { + Camera3D camera = new Camera3D(universe); + camera.setViewPoint(new Position3D(cameraX, cameraY, cameraZ)); + camera.addTarget(new Position3D(0.0, 0.0, 0.0)); + camera.adjust(0L); + attachCamera(camera); + } + + void createLight(Universe universe) { + // 環境光 + AmbientLight amblight = new AmbientLight(new Color3f(0.5f, 0.5f, 0.5f)); + amblight + .setInfluencingBounds(new BoundingSphere(new Point3d(), 10000.0)); + universe.placeLight(amblight); + // 平行光源 + DirectionalLight dirlight = new DirectionalLight(true, // 光のON/OFF + new Color3f(1.0f, 1.0f, 1.0f), // 光の色 + new Vector3f(0.0f, -1.0f, -0.5f) // 光の方向ベクトル + ); + dirlight + .setInfluencingBounds(new BoundingSphere(new Point3d(), 10000.0)); + universe.placeLight(dirlight); + } + + public void setObject(BaseObject3D obj) { + BranchGroup newObj = new BranchGroup(); + newObj.setCapability(BranchGroup.ALLOW_DETACH); + newObj.addChild(obj.getTransformGroupToPlace()); + objRoot.removeAllChildren(); + objRoot.addChild(newObj); + } +} diff --git a/src/main/java/framework/RWT/RWTSelectionManager.java b/src/main/java/framework/RWT/RWTSelectionManager.java new file mode 100644 index 0000000..bab115d --- /dev/null +++ b/src/main/java/framework/RWT/RWTSelectionManager.java @@ -0,0 +1,266 @@ +package framework.RWT; + + +public class RWTSelectionManager { + + private int size = 0; + + private WidgetHolder selectedHolder = new WidgetHolder(null, 0, 0); + + private WidgetHolder upMostHolder; + private WidgetHolder leftMostHolder; + + private RWTSelector selector = new DefaultSelector(); + + public void add(RWTSelectableWidget widget, int m, int n) { + WidgetHolder holder = new WidgetHolder(widget, m, n); + if(size == 0) { + widget.selected(); + selectedHolder = holder; + upMostHolder = holder; + leftMostHolder = holder; + selector.setSelectableWidget(selectedHolder.getSelectableWidget()); + } + else { + //どれくらい下か + connectY(holder); + + //どれくらい右か + connectX(holder); + } + size++; + } + + public void up() { + if(selectedHolder.hasUp()) { + selectedHolder.getSelectableWidget().deselected(); + selectedHolder = selectedHolder.getUp(); + selectedHolder.getSelectableWidget().selected(); + selector.setSelectableWidget(selectedHolder.getSelectableWidget()); + } + } + + public void down() { + if(selectedHolder.hasDown()) { + selectedHolder.getSelectableWidget().deselected(); + selectedHolder = selectedHolder.getDown(); + selectedHolder.getSelectableWidget().selected(); + selector.setSelectableWidget(selectedHolder.getSelectableWidget()); + } + } + + public void right() { + if(selectedHolder.hasRight()) { + selectedHolder.getSelectableWidget().deselected(); + selectedHolder = selectedHolder.getRight(); + selectedHolder.getSelectableWidget().selected(); + selector.setSelectableWidget(selectedHolder.getSelectableWidget()); + } + } + + public void left() { + if(selectedHolder.hasLeft()) { + selectedHolder.getSelectableWidget().deselected(); + selectedHolder = selectedHolder.getLeft(); + selectedHolder.getSelectableWidget().selected(); + selector.setSelectableWidget(selectedHolder.getSelectableWidget()); + } + } + + public RWTWidget getSelectedWidget() { + if(selectedHolder != null) { + return selectedHolder.getWidget(); + } + else { + return null; + } + } + + public void setSelector(RWTSelector c) { + selector = c; + } + + public RWTSelector getSelector() { + return selector; + } + + /** + * 左右をつなげる + * @param newHolder + */ + private void connectX(WidgetHolder newHolder) { + WidgetHolder holder = leftMostHolder; + while(true) { + if(newHolder.getY() < holder.getY()) { + //holderのほうが左 + if(holder.getLeft() != null) { + holder.getLeft().setRight(newHolder); + } + newHolder.setLeft(holder.getLeft()); + + holder.setLeft(newHolder); + newHolder.setRight(holder); + return; + } + else if(newHolder.getY() == holder.getY()) { + if(newHolder.getX() < holder.getX()) { + //holderのほうが左 + if(holder.getLeft() != null) { + holder.getLeft().setRight(newHolder); + } + newHolder.setLeft(holder.getLeft()); + + holder.setLeft(newHolder); + newHolder.setRight(holder); + return; + } + } + //まだ右のやつはいる? + if(holder.hasRight()) { + holder = holder.getRight(); + } + //もう右のやつはいない + else { + holder.setRight(newHolder); + newHolder.setLeft(holder); + return; + } + } + } + + /** + * 上下をつなげる + * @param newHolder + */ + private void connectY(WidgetHolder newHolder) { + WidgetHolder holder = upMostHolder; + while(true) { + if(newHolder.getX() < holder.getX()) { + //holderのほうが左 + if(holder.getUp() != null) { + holder.getUp().setDown(newHolder); + } + newHolder.setUp(holder.getUp()); + + holder.setUp(newHolder); + newHolder.setDown(holder); + return; + } + else if(newHolder.getX() == holder.getX()) { + if(newHolder.getY() < holder.getY()) { + //holderのほうが左 + if(holder.getUp() != null) { + holder.getUp().setDown(newHolder); + } + newHolder.setUp(holder.getUp()); + + holder.setUp(newHolder); + newHolder.setDown(holder); + return; + } + } + //まだ下のやつはいる? + if(holder.hasDown()) { + holder = holder.getDown(); + } + //もう下のやつはいない + else { + holder.setDown(newHolder); + newHolder.setUp(holder); + return; + } + } + } + + private class WidgetHolder { + private RWTSelectableWidget widget; + + //行 + private int x; + //列 + private int y; + + private WidgetHolder up = null; + private WidgetHolder down = null; + private WidgetHolder right = null; + private WidgetHolder left = null; + + public WidgetHolder(RWTSelectableWidget r, int m, int n) { + widget = r; + x = m; + y = n; + } + + //getter + public RWTWidget getWidget() { + return (RWTWidget) widget; + } + + public RWTSelectableWidget getSelectableWidget() { + return widget; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public WidgetHolder getUp() { + return up; + } + public WidgetHolder getDown() { + return down; + } + public WidgetHolder getRight() { + return right; + } + public WidgetHolder getLeft() { + return left; + } + + //setter + public void setUp(WidgetHolder d) { + up = d; + } + public void setDown(WidgetHolder d) { + down = d; + } + public void setRight(WidgetHolder d) { + right = d; + } + public void setLeft(WidgetHolder d) { + left = d; + } + + public boolean hasUp() { + if(up == null) { + return false; + } + return true; + } + + public boolean hasDown() { + if(down == null) { + return false; + } + return true; + } + + public boolean hasRight() { + if(right == null) { + return false; + } + return true; + } + + public boolean hasLeft() { + if(left == null) { + return false; + } + return true; + } + } +} diff --git a/src/main/java/framework/RWT/RWTSelector.java b/src/main/java/framework/RWT/RWTSelector.java new file mode 100644 index 0000000..bfdc8e0 --- /dev/null +++ b/src/main/java/framework/RWT/RWTSelector.java @@ -0,0 +1,47 @@ +package framework.RWT; + +import java.awt.*; + +public abstract class RWTSelector extends RWTWidget { + protected float relativeX = 0.0f; + protected float relativeY = 0.0f; + protected float relativeWidth = 0.0f; + protected float relativeHeight = 0.0f; + protected int x = 0; + protected int y = 0; + protected int width = 0; + protected int height = 0; + + protected RWTSelectableWidget widget = null; + + public void setSelectableWidget(RWTSelectableWidget w) { + widget = w; + } + + public boolean hasWidget() { + if(widget == null) { + return false; + } + return true; + } + + @Override + public void adjust(Component parent) { + x = widget.getAbsoluteX(); + y = widget.getAbsoluteY(); + width = widget.getAbsoluteWidth(); + height = widget.getAbsoluteHeight(); + } + + @Override + public boolean isVisible() { + if(hasWidget()) { + x = widget.getAbsoluteX(); + y = widget.getAbsoluteY(); + width = widget.getAbsoluteWidth(); + height = widget.getAbsoluteHeight(); + return visible; + } + return false; + } +} diff --git a/src/main/java/framework/RWT/RWTVirtualController.java b/src/main/java/framework/RWT/RWTVirtualController.java new file mode 100644 index 0000000..86d45b1 --- /dev/null +++ b/src/main/java/framework/RWT/RWTVirtualController.java @@ -0,0 +1,116 @@ +package framework.RWT; + +import java.awt.event.KeyEvent; + + +public class RWTVirtualController { + + private boolean bKeyDown[][] = { + {false, false, false, false, false, false, false, false}, + {false, false, false, false, false, false, false, false}}; + private boolean rawKeyDown[] = new boolean[256]; + private boolean mouseButtonDown1 = false; + private boolean mouseButtonDown2 = false; + private boolean mouseButtonDown3 = false; + private double mouseX = 0; + private double mouseY = 0; + private static RWTVirtualKey keyMap[] = new RWTVirtualKey[256]; + + public static final int UP = 0; + public static final int DOWN = 1; + public static final int RIGHT = 2; + public static final int LEFT = 3; + public static final int BUTTON_A = 4; + public static final int BUTTON_B = 5; + public static final int BUTTON_C = 6; + public static final int BUTTON_D = 7; + + public RWTVirtualController() { + // player1用のkeyBindの初期化 + RWTVirtualController.keyBind(KeyEvent.VK_W, 0, RWTVirtualController.UP); // w + RWTVirtualController.keyBind(KeyEvent.VK_D, 0, RWTVirtualController.RIGHT); //d + RWTVirtualController.keyBind(KeyEvent.VK_A, 0, RWTVirtualController.LEFT); // a + RWTVirtualController.keyBind(KeyEvent.VK_S, 0, RWTVirtualController.DOWN); // s + RWTVirtualController.keyBind(KeyEvent.VK_V, 0, RWTVirtualController.BUTTON_B); // v + RWTVirtualController.keyBind(KeyEvent.VK_B, 0, RWTVirtualController.BUTTON_A); // b + RWTVirtualController.keyBind(KeyEvent.VK_SPACE, 0, RWTVirtualController.BUTTON_C); // space + RWTVirtualController.keyBind(KeyEvent.VK_E, 0, RWTVirtualController.BUTTON_D); // e + + // player2用のkeyBindの初期化 + RWTVirtualController.keyBind(KeyEvent.VK_O, 1, RWTVirtualController.UP); // o + RWTVirtualController.keyBind(KeyEvent.VK_SEMICOLON, 1, RWTVirtualController.RIGHT); //; + RWTVirtualController.keyBind(KeyEvent.VK_K, 1, RWTVirtualController.LEFT); // k + RWTVirtualController.keyBind(KeyEvent.VK_L, 1, RWTVirtualController.DOWN); // l + RWTVirtualController.keyBind(KeyEvent.VK_BACK_SLASH, 1, RWTVirtualController.BUTTON_B); // \ + RWTVirtualController.keyBind(KeyEvent.VK_SHIFT, 1, RWTVirtualController.BUTTON_A); // shift + RWTVirtualController.keyBind(KeyEvent.VK_CONTROL, 1, RWTVirtualController.BUTTON_C); // ctrl + RWTVirtualController.keyBind(KeyEvent.VK_P, 1, RWTVirtualController.BUTTON_D); // p + } + + static public void keyBind(int keyCode, int playerNo, int buttonNo) { + keyMap[keyCode] = new RWTVirtualKey(playerNo, keyCode, buttonNo); + } + + public boolean isKeyDown(int player, int keyNo) { + return bKeyDown[player][keyNo]; + } + + public boolean isKeyDown(int keyCode) { + return rawKeyDown[keyCode]; + } + + public void setKeyDown(int keyCode, boolean b) { + if (keyMap[keyCode] != null) { + bKeyDown[keyMap[keyCode].getPlayer()][keyMap[keyCode].getVirtualKey()] = b; + } + rawKeyDown[keyCode] = b; + } + + public RWTVirtualKey getVirtualKey(int keyCode) { + return keyMap[keyCode]; + } + + public boolean isMouseButtonDown(int buttonNo) { + switch (buttonNo) { + case 0: + return mouseButtonDown1; + case 1: + return mouseButtonDown2; + case 2: + return mouseButtonDown3; + } + return false; + } + + public void setMouseButtonDown(int buttonNo, boolean b) { + switch (buttonNo) { + case 0: + mouseButtonDown1 = b; + break; + case 1: + mouseButtonDown2 = b; + break; + case 2: + mouseButtonDown3 = b; + break; + } + } + + public double getMouseX() { + return mouseX; + } + + public double getMouseY() { + return mouseY; + } + + public void setMousePosition(double x, double y) { + mouseX = x; + mouseY = y; + } + + public void moveMousePosition(double dx, double dy) { + mouseX += dx; + mouseY += dy; + } +} diff --git a/src/main/java/framework/RWT/RWTVirtualKey.java b/src/main/java/framework/RWT/RWTVirtualKey.java new file mode 100644 index 0000000..ee2146d --- /dev/null +++ b/src/main/java/framework/RWT/RWTVirtualKey.java @@ -0,0 +1,43 @@ +package framework.RWT; + + +public class RWTVirtualKey { + + private int player; + private int physicalKey; + private int virtualKey; + + public RWTVirtualKey(int player, int physicalKey, int virtualKey){ + setPlayer(player); + setPhysicalKey(physicalKey); + setVirtualKey(virtualKey); + } + + public void setPlayer(int player) { + this.player = player; + } + + public int getPlayer() { + return player; + } + + public void setPhysicalKey(int physicalKey) { + this.physicalKey = physicalKey; + } + + public int getPhysicalKey() { + return physicalKey; + } + + public void setVirtualKey(int virtualKey) { + this.virtualKey = virtualKey; + } + + public int getVirtualKey() { + return virtualKey; + } + + public void assignPlayer(int playernum) { + + } +} diff --git a/src/main/java/framework/RWT/RWTWidget.java b/src/main/java/framework/RWT/RWTWidget.java new file mode 100644 index 0000000..15a9b76 --- /dev/null +++ b/src/main/java/framework/RWT/RWTWidget.java @@ -0,0 +1,37 @@ +package framework.RWT; + +import java.awt.*; + +/** + * 親コンテナに対して相対座標で配置できるGUI部品 + * @author Wataru + * + */ +public abstract class RWTWidget { + protected Color color = Color.BLACK; + + protected boolean visible = true; + + /** + * 親コンテナ(キャンバスの場合もある)の大きさに合わせて位置とサイズを調整する + * @param parent + */ + public abstract void adjust(Component parent); + public abstract void paint(Graphics g); + + /** + * 色を設定する + * @param c + */ + public void setColor(Color c) { + color = c; + } + + public void setVisible(boolean b) { + visible = b; + } + + public boolean isVisible() { + return visible; + } +} diff --git a/src/main/java/framework/animation/Animation3D.java b/src/main/java/framework/animation/Animation3D.java new file mode 100644 index 0000000..51d3ab6 --- /dev/null +++ b/src/main/java/framework/animation/Animation3D.java @@ -0,0 +1,137 @@ +package framework.animation; + +import framework.model3D.Position3D; +import framework.model3D.Quaternion3D; + +import java.util.ArrayList; + +/** + * 階層化されたオブジェクトに対するアニメーション情報を保持する(部品単位で位置、向き、テクスチャをアニメーション可能) + * @author 新田直也 + * + */ +public class Animation3D { + public long time = 0; + + private ArrayList partList = new ArrayList(); + private long maxKey = getMaxKey(); + + public Animation3D() { + time = 0; + } + + public Animation3D(Animation3D a) { + time = 0; + partList = a.partList; + maxKey = a.maxKey; + } + + public boolean equals(Animation3D a) { + return (partList == a.partList && maxKey == a.maxKey); + } + + public void reset() { + time = 0; + } + + public boolean progress(long interval) { + if (maxKey == 0) + return true; // 空のアニメーションの場合動かさない + time += interval; + // System.out.println(time + "/" + maxKey); + if (time > maxKey) { + time = time % maxKey; // timeが最後の要素のkeyの値(アニメーションの最後のkey)を超えている場合、tを最後のkeyの値(maxKey)で割った余りとして設定する + return false; + } else + return true; + } + + public Pose3D getPose() { + if (maxKey == 0 || partList.size() == 0) + return new DefaultPose3D(); + + KeyFrame[] aroundKey = new KeyFrame[2]; + Quaternion3D q; + Position3D p; + Quaternion3D tq; + Position3D tp; + Pose3D pose = new Pose3D(); + for (int i = 0; i < partList.size(); i++) { + aroundKey = partList.get(i).getKey(time); + + // コピーコンストラクタの作成 + q = null; + p = null; + tq = null; + tp = null; + if (aroundKey[0].getPosition() != null) { + p = new Position3D(aroundKey[0].getPosition()); // getPosition()が参照を返すのでコピーしてから変更 + } + if (aroundKey[0].getQuaternion() != null) { + q = new Quaternion3D(aroundKey[0].getQuaternion()); // getQuaternion()がアドレス(参照)を返すので、コピー(=q)を作成してそちらを変更している。 + } + if (aroundKey[0].getTexturePosition() != null) { + tp = new Position3D(aroundKey[0].getTexturePosition()); // getPosition()が参照を返すのでコピーしてから変更 + } + if (aroundKey[0].getTextureQuaternion() != null) { + tq = new Quaternion3D(aroundKey[0].getTextureQuaternion()); // getQuaternion()がアドレス(参照)を返すので、コピー(=q)を作成してそちらを変更している。 + } + + // timeがkeyそのものだった場合(補間の計算が不要な場合) + if (aroundKey[1] != null) { + // t1 はtの前のkey(aroundKey[0])のスカラー倍 + double t1 = aroundKey[1].key - time; + // t2 はtの後のkey(aroundKey[1])のスカラー倍 + double t2 = time - aroundKey[0].key; + double t3 = aroundKey[1].key - aroundKey[0].key; + double timealpha = t2 / t3; + + // timeに対するQuaternionとPositionの計算 + if (p != null) { + Position3D p2 = new Position3D(aroundKey[1].getPosition()); + p.mul(t1 / t3).add(p2.mul(t2 / t3)); + } + if (q != null) { + Quaternion3D q2 = new Quaternion3D(aroundKey[1].getQuaternion()); + q.getInterpolate(q2, timealpha); + } + if (tp != null) { + Position3D tp2 = new Position3D(aroundKey[1].getTexturePosition()); + tp.mul(t1 / t3).add(tp2.mul(t2 / t3)); + } + if (tq != null) { + Quaternion3D tq2 = new Quaternion3D(aroundKey[1].getTextureQuaternion()); + tq.getInterpolate(tq2, timealpha); + } + } + pose.addPose(partList.get(i).getName(), p, q, aroundKey[0].getTexture(), tp, tq, partList.get(i).getTextureUnit()); + } + + return pose; + } + + public Animation3D merge(Animation3D a) { + this.partList.addAll(a.partList); + maxKey = getMaxKey(); + return this; + } + + public void addPartAnimation(PartAnimation pa) { + partList.add(pa); + maxKey = getMaxKey(); + } + + // アニメーションが終了したかを判定するためにkeyの最大値を探索して返すメソッド + private long getMaxKey() { + long maxKey = 0; + int i; + + for (i = 0; i < partList.size(); i++) { + if (maxKey < partList.get(i).getLastKey()) { + maxKey = partList.get(i).getLastKey(); + } else + continue; + } + return maxKey; + } +} diff --git a/src/main/java/framework/animation/AnimationFactory.java b/src/main/java/framework/animation/AnimationFactory.java new file mode 100644 index 0000000..fb60ff2 --- /dev/null +++ b/src/main/java/framework/animation/AnimationFactory.java @@ -0,0 +1,123 @@ +package framework.animation; + +import com.sun.j3d.utils.universe.SimpleUniverse; +import cv97.SceneGraph; +import cv97.j3d.SceneGraphJ3dObject; +import cv97.node.Node; +import cv97.node.OrientationInterpolatorNode; +import cv97.node.PositionInterpolatorNode; +import cv97.util.LinkedListNode; +import framework.model3D.Position3D; +import framework.model3D.Quaternion3D; +import framework.schedule.ScheduleManager; +import framework.schedule.TaskController; + +import javax.media.j3d.Canvas3D; +import javax.media.j3d.View; + +public class AnimationFactory { + private static Canvas3D canvas = new Canvas3D(SimpleUniverse.getPreferredConfiguration()); + + public static Animation3D loadAnimation(String fileName){ + + SceneGraph sg = new SceneGraph(SceneGraph.NORMAL_GENERATION); + + // ファイル読み込み時にレンダリングが行われるので、オフスクリーンレンダリングを禁止する + TaskController renderingController = ScheduleManager.getInstance().getController("rendering"); + renderingController.waitForActivation(); + renderingController.activate(); + + // ファイル読み込み + View view = canvas.getView(); + if (view != null) view.removeCanvas3D(canvas); + SceneGraphJ3dObject sgObject = new SceneGraphJ3dObject(canvas, sg); + sg.setObject(sgObject); + sg.load(fileName); + + // オフスクリーンレンダリングの許可 + renderingController.deactivate(); + + // Animation3Dへの変換 + Animation3D animation= loadAnimation(sg); + + return animation; + } + + private static Animation3D loadAnimation(LinkedListNode node){ + Animation3D mergedAnimation = null; + System.out.println(node.getClass()); + + if( ! (node instanceof PositionInterpolatorNode || node instanceof OrientationInterpolatorNode) ){ + Node childNodes; + String name = ""; + if (node instanceof SceneGraph) { + childNodes = ((SceneGraph)node).getNodes(); + } else { + childNodes = ((Node)node).getChildNodes(); + name = ((Node)node).getName(); + } + if(childNodes != null){ + + for(Node n = childNodes;n != null;n = n.next()){ + Animation3D animation; + if((animation = loadAnimation(n)) != null){ + if (mergedAnimation != null) { + mergedAnimation.merge(animation); + } else { + mergedAnimation = animation; + } + } + + //兄弟で無限ループにならないようにする処理 + Node dummy = n; + dummy = dummy.next(); + if(n == dummy){ + break; + } + } + +// System.out.println("子供の数は"+ list.size()); + + if(mergedAnimation != null){ + return mergedAnimation; + } + } + return null; + } else if (node instanceof PositionInterpolatorNode) { + + String name = ((PositionInterpolatorNode)node).getName(); + PositionInterpolatorNode pi = (PositionInterpolatorNode)node; + + //アニメーションデータの取得 + int i=0; + PartAnimation pa = new PartAnimation(name); + for(i=0;i keyList = new ArrayList(); + public static final int SINGLE_TEXTURE = -1; + + public PartAnimation(String name) { + this.name = name; + textureUnit = SINGLE_TEXTURE; + } + + public PartAnimation(String name, int textureUnit) { + this.name = name; + this.textureUnit = textureUnit; + } + + String getName() { + return name; + } + + int getTextureUnit() { + return textureUnit; + } + + //アニメーションの経過時間の前後のkeyをKeyFrame型の配列で取得するメソッド + KeyFrame[] getKey(long t){ + int i; + KeyFrame[] aroundKey = new KeyFrame[2]; + + for(i=1;i partPoseList = new ArrayList(); + + public Pose3D() { + } + + public Pose3D(Pose3D p) { + partPoseList = new ArrayList(p.partPoseList); + } + + public void applyTo(Object3D obj) { + // obj.rotate(part, vx, vy, vz, a); + int n = partPoseList.size(); + for (int i = 0; i < n; i++) { + final PartPose partpose = (PartPose) partPoseList.get(i); + Object3D partObj = obj.getPart(partpose.name); + if (partObj != null) { + // 位置 + if (partpose.position != null) + partObj.apply(partpose.position, false); + // 向き + if (partpose.quaternion != null) + partObj.apply(partpose.quaternion, false); + // テクスチャ + if (partpose.texture != null + || partpose.texturePosition != null + || partpose.textureQuaternion != null) { + ObjectVisitor v = new ObjectVisitor() { + @Override + public void postVisit(Object3D obj) { + Appearance appearance = null; + Node node = obj.getPrimitiveNode(); + if (node != null && node instanceof Shape3D) { + appearance = ((Shape3D)node).getAppearance(); + } else if (node != null && node instanceof Primitive) { + appearance = ((Primitive)node).getAppearance(); + } + if (appearance != null) { + TextureUnitState tu = null; + if (partpose.texture != null) { + if (partpose.textureUnit == PartAnimation.SINGLE_TEXTURE) { + appearance.setTexture(partpose.texture); + } else { + tu = appearance.getTextureUnitState(partpose.textureUnit); + tu.setTexture(partpose.texture); + appearance.setTextureUnitState(partpose.textureUnit, tu); + } + } + if ((partpose.texturePosition != null || partpose.textureQuaternion != null) + && obj.hasAppearancePrepared()) { // Appearance が初回のレンダリングの直前で更新される可能性があるため + TextureAttributes ta; + if (partpose.textureUnit == PartAnimation.SINGLE_TEXTURE) { + ta = appearance.getTextureAttributes(); + } else { + tu = appearance.getTextureUnitState(partpose.textureUnit); + ta = tu.getTextureAttributes(); + } + if (ta == null) { + ta = new TextureAttributes(); + ta.setCapability(TextureAttributes.ALLOW_TRANSFORM_READ); + ta.setCapability(TextureAttributes.ALLOW_TRANSFORM_WRITE); + } + Transform3D t = new Transform3D(); + if (partpose.texturePosition != null) { + t.set(new Vector3d(partpose.texturePosition.getVector3d())); + } + if (partpose.textureQuaternion != null) { + Transform3D t2 = new Transform3D(); + t2.set(partpose.textureQuaternion.getQuat()); + t.mul(t2); + } +// shape.getAppearance().getTextureAttributes().setTextureTransform(t); + ta.setTextureTransform(t); + if (partpose.textureUnit == PartAnimation.SINGLE_TEXTURE) { + appearance.setTextureAttributes(ta); + } else { + tu.setTextureAttributes(ta); + appearance.setTextureUnitState(partpose.textureUnit, tu); + } + } + } + } + @Override + public void preVisit(Object3D obj) { + } + }; + partObj.accept(v); + } + } + } + } + + public void addPose(String name, Position3D p, Quaternion3D q, Texture texture, Position3D tp, Quaternion3D tq, int textureUnit) { + PartPose partpose = new PartPose(name, p, q, texture, tp, tq, textureUnit); + partPoseList.add(partpose); + } + + @Override + public Property3D clone() { + // TODO Auto-generated method stub + return new Pose3D(this); + } +} diff --git a/src/main/java/framework/audio/BGM3D.java b/src/main/java/framework/audio/BGM3D.java new file mode 100644 index 0000000..94bc8c7 --- /dev/null +++ b/src/main/java/framework/audio/BGM3D.java @@ -0,0 +1,15 @@ +package framework.audio; + +public class BGM3D { + private static Sound3D currentBGM = null; + + public static Sound3D registerBGM(String fileName) { + return new Sound3D(fileName); + } + + public static void playBGM(Sound3D newBGM) { + if (currentBGM != null) currentBGM.stop(); + newBGM.loopPlay(); + currentBGM = newBGM; + } +} diff --git a/src/main/java/framework/audio/Sound3D.java b/src/main/java/framework/audio/Sound3D.java new file mode 100644 index 0000000..0f80362 --- /dev/null +++ b/src/main/java/framework/audio/Sound3D.java @@ -0,0 +1,53 @@ +package framework.audio; + +import javax.sound.sampled.*; +import java.io.File; +import java.io.IOException; + + +public class Sound3D { + private Clip clip = null; + + public Sound3D(String fileName) { + Clip clip = null; + AudioInputStream aistream; + try { + File file = new File(fileName); + aistream = AudioSystem.getAudioInputStream(file); + DataLine.Info info = new DataLine.Info(Clip.class, aistream.getFormat()); + clip = (Clip)AudioSystem.getLine(info); + clip.open(aistream); + } catch (UnsupportedAudioFileException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (LineUnavailableException e) { + e.printStackTrace(); + } + this.clip = clip; + } + + public void play() { + clip.stop(); + clip.setFramePosition(0); + clip.start(); + } + + public void play(double vol) { + FloatControl control = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN); + control.setValue((float)Math.log10(vol) * 20); + clip.stop(); + clip.setFramePosition(0); + clip.start(); + } + + public void loopPlay() { + clip.stop(); + clip.setFramePosition(0); + clip.loop(Clip.LOOP_CONTINUOUSLY); + } + + public void stop() { + clip.stop(); + } +} diff --git a/src/main/java/framework/gameMain/AbstractGame.java b/src/main/java/framework/gameMain/AbstractGame.java new file mode 100644 index 0000000..dd7de27 --- /dev/null +++ b/src/main/java/framework/gameMain/AbstractGame.java @@ -0,0 +1,96 @@ +package framework.gameMain; + +import framework.RWT.RWTFrame3D; +import framework.schedule.ScheduleManager; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * ゲームの基本クラス + * @author 新田直也 + * + */ +public abstract class AbstractGame implements Runnable { + protected RWTFrame3D mainFrame; + private ScheduledExecutorService t; + private long minimumFrameTime = 5; + private long maximumFrameTime = 15; + private boolean bWaitForRender = true; + private long prevTime = 0L; + + public abstract RWTFrame3D createFrame3D(); + protected abstract IGameState getCurrentGameState(); + + public AbstractGame() { + mainFrame = createFrame3D(); + } + + public void start() { + if (getCurrentGameState() != null) { + activateState(getCurrentGameState()); + } + } + + public void stop() { + if (getCurrentGameState() != null) { + deactivateState(getCurrentGameState()); + } + } + + public void setFramePolicy(long minimumFrameTime, boolean bWaitForRender) { + this.minimumFrameTime = minimumFrameTime; + this.bWaitForRender = bWaitForRender; + } + + public void setFramePolicy(long minimumFrameTime, long maximumFrameTime, boolean bWaitForRender) { + this.minimumFrameTime = minimumFrameTime; + this.maximumFrameTime = maximumFrameTime; + this.bWaitForRender = bWaitForRender; + } + + public void run() { + if (bWaitForRender) { + // レンダリングの終了と同期してゲームを進める + ScheduleManager.getInstance().getController("rendering").waitForActivation(); + } + mainFrame.doRender(); + long curTime = System.currentTimeMillis(); + long interval; + if (prevTime == 0L) { + interval = minimumFrameTime; + } else { + interval = curTime - prevTime; + if (interval > maximumFrameTime) { + interval = maximumFrameTime; + } + } + prevTime = curTime; + // 今のゲーム状態のupdateを呼ぶ。 + getCurrentGameState().update(mainFrame.getVirtualController(), interval); + } + + protected void activateState(IGameState s) { + s.init(mainFrame); + if(s.useTimer()){ + timerRun(); + } + } + + protected void deactivateState(IGameState s) { + if(s.useTimer()){ + timerCancel(); + } + } + + private void timerCancel() { + if (t != null) t.shutdownNow(); + } + + private void timerRun() { + t = new ScheduledThreadPoolExecutor(1); + // scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) + t.scheduleWithFixedDelay(this, minimumFrameTime, minimumFrameTime, TimeUnit.MILLISECONDS); + } +} \ No newline at end of file diff --git a/src/main/java/framework/gameMain/AbstractGameState.java b/src/main/java/framework/gameMain/AbstractGameState.java new file mode 100644 index 0000000..5b7a9d3 --- /dev/null +++ b/src/main/java/framework/gameMain/AbstractGameState.java @@ -0,0 +1,46 @@ +package framework.gameMain; + +import framework.RWT.RWTCanvas3D; +import framework.RWT.RWTContainer; +import framework.RWT.RWTFrame3D; +import framework.RWT.RWTVirtualController; + +import javax.media.j3d.GraphicsConfigTemplate3D; +import java.awt.*; + +public abstract class AbstractGameState implements IGameState { + protected RWTContainer container; + + public void init(RWTFrame3D frame) { + if (container == null) { + container = createContainer(); + } + frame.setContentPane(container); + GraphicsConfiguration gc = null; + if (frame.isShadowCasting()) { + // 影を付ける場合 + // ステンシルバッファを使用する GraphicsConfiguration の生成 + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice gd = ge.getDefaultScreenDevice(); + GraphicsConfigTemplate3D gct3D = new GraphicsConfigTemplate3D(); + gct3D.setStencilSize(8); + gc = gd.getBestConfiguration(gct3D); + } + container.build(gc); + } + + public abstract RWTContainer createContainer(); + /* (non-Javadoc) + * @see IGameState#useTimer() + */ + public abstract boolean useTimer(); + /* (non-Javadoc) + * @see IGameState#progress(RWTVirtualController) + */ + public abstract void update(RWTVirtualController virtualController, long interval); + + public RWTCanvas3D getRWTCanvas3D() { + if (container == null) return null; + return container.getPrimaryRWTCanvas3D(); + } +} diff --git a/src/main/java/framework/gameMain/AbstractManager.java b/src/main/java/framework/gameMain/AbstractManager.java new file mode 100644 index 0000000..81a5aa2 --- /dev/null +++ b/src/main/java/framework/gameMain/AbstractManager.java @@ -0,0 +1,205 @@ +package framework.gameMain; + +import java.io.*; +import java.util.ArrayList; + +public abstract class AbstractManager { + + private DataBox data = new DataBox(); + private BufferedReader reader = null; + private String seekFilename; + + protected void addSeekFile(String filename) { + seekFilename = filename; + } + + protected void seek(String d) { + File file = new File(d); + //ディレクトリのとき + if(file.isDirectory()) { + openDirectory(file.listFiles()); + } + //ファイルのとき + else { + openFile(file); + } + } + + //ディレクトリをあける + private void openDirectory(File[] fileList) { + File file; + for(int i = 0; i < fileList.length; i++) { + file = fileList[i]; + //ディレクトリのとき + if(file.isDirectory()) { + openDirectory(file.listFiles()); + } + //ファイルのとき + else { + openFile(file); + } + } + } + //ファイルをあける + private void openFile(File file) { + if(file.getName().equals(seekFilename)) { + data.removeAll(); + + try { + reader = new BufferedReader(new FileReader(file)); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + + findFile(file); + + try { + reader.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + /** + * setBufferedReader(File file)をすること + */ + protected String readLine() { + if(reader != null) { + try { + return reader.readLine(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; + } + + protected void setTags(ArrayList tags) { + for(int i = 0; i < tags.size(); i++) { + data.setTag(tags.get(i)); + } + } + protected void setTag(String tag) { + data.setTag(tag); + } + + protected void findFile(File file) { + String str; + while(true) { + str = readLine(); + if(str == null) { + break; + } + String[] strings = str.split(":"); + if(strings.length >= 2) { + data.setData(strings[0], strings[1]); + } + } + create(); + } + + protected String getData(String tag) { + return data.getData(tag); + } + + public boolean hasData(String tag){ + if(data.getData(tag) == null) + return false; + else if(data.getData(tag) == "") + return false; + else return true; + } + + abstract protected void create(); + + private class DataBox { + private ArrayList dataList = new ArrayList(); + + public void setTag(String tag) { + dataList.add(new Data(tag)); + } + + public void setData(String tag, String data) { + for(int i = 0; i < dataList.size(); i++) { + if(dataList.get(i).getTag().equals(tag)) { + dataList.get(i).addData(data); + } + } + } + + /** + * このkeyのデータを返す。 + * @param key タグ名 + * @return + */ + public String getData(String tag) { + for(int i = 0; i < dataList.size(); i++) { + if(dataList.get(i).getTag().equals(tag)) { + if(dataList.get(i).getData().size() > 0) { + return dataList.get(i).getData().get(0); + } + else { + break; + } + } + } + return ""; + } + + public ArrayList getDatas(String tag) { + for(int i = 0; i < dataList.size(); i++) { + if(dataList.get(i).getTag().equals(tag)) { + return dataList.get(i).getData(); + } + } + return new ArrayList(); + } + + /** + * このタグが存在するか。 + * @param tag + * @return + */ + public boolean isExist(String tag) { + for(int i = 0; i < dataList.size(); i++) { + if(dataList.get(i).getTag().equals(tag)) { + return true; + } + } + return false; + } + + public void removeAll() { + for(int i = 0; i < dataList.size(); i++) { + dataList.get(i).removeData(); + } + } + } + + private class Data { + private String tag; + private ArrayList data = new ArrayList(); + + public Data(String tag) { + this.tag = tag; + } + + public String getTag() { + return tag; + } + + public void addData(String d) { + data.add(d); + } + + public ArrayList getData() { + return data; + } + + public void removeData() { + data = new ArrayList(); + } + } +} diff --git a/src/main/java/framework/gameMain/Actor.java b/src/main/java/framework/gameMain/Actor.java new file mode 100644 index 0000000..b185b4b --- /dev/null +++ b/src/main/java/framework/gameMain/Actor.java @@ -0,0 +1,337 @@ +package framework.gameMain; + + +import framework.animation.Animation3D; +import framework.model3D.*; +import framework.physics.*; + +import javax.media.j3d.Transform3D; +import javax.vecmath.AxisAngle4d; +import javax.vecmath.Vector3d; +import java.util.ArrayList; + +/** + * ゲーム内の登場物全般 + * @author 新田直也 + */ +public abstract class Actor extends Animatable implements Movable { + protected Vector3d direction = new Vector3d(1.0, 0.0, 0.0); + protected Mode mode; + // 以下省メモリ化のため予めインスタンスを生成 + protected Mode modeFreeFall = new ModeFreeFall(); + protected Mode modeOnGround = new ModeOnGround(); + + public Actor(Object3D body, Animation3D animation) { + super(new Solid3D(body), animation); + mode = modeOnGround; + } + + public Actor(Solid3D body, Animation3D animation) { + super(body, animation); + mode = modeOnGround; + } + + public Actor(ActorModel model) { + super(model.createBody(), model.getAnimation()); + mode = modeOnGround; + } + + /** + * 単位時間ごとの動作(衝突判定処理は行わない) + * @param interval --- 前回呼び出されたときからの経過時間(ミリ秒単位) + */ + public void motion(long interval) { + // 1. 位置を動かす + ((Solid3D) body).move(interval, getGravity(), getGravityCenter()); + super.motion(interval); + } + + /** + * 単位時間ごとの動作(衝突判定処理も行う) + * @param interval --- 前回呼び出されたときからの経過時間(ミリ秒単位) + * @param ground --- 地面(構造物) + */ + public void motion(long interval, Ground ground) { + // 1. 位置を動かす + ((Solid3D) body).move(interval, getGravity(), getGravityCenter()); + + if (animation != null) { + // 2. アニメーションを実行 + if (animation.progress(interval) == false) { + onEndAnimation(); + } + + // 3. 姿勢を変える + body.apply(animation.getPose(), false); + } + + // 4. 衝突判定 + CollisionResult cr = PhysicsUtility.doesIntersect((Solid3D) body, ground); + + // 5. 衝突応答 + if (cr != null) { + // 構造物にぶつかった、または接触している時 + if (cr.normal.dot(PhysicsUtility.vertical) > GeometryUtility.TOLERANCE) { + // 上向きの面(=地面)にぶつかった、または接触している時 + if (cr.length <= GeometryUtility.TOLERANCE) { + // 地面に乗っている + if (!(mode instanceof ModeOnGround)) { + // 落ちてきて丁度乗った + mode = modeOnGround; + ((ModeOnGround) mode).setNormal(cr.normal); + onEndFall(); + } + } else { + // 地面にめり込んだ + // 5.1. 押し戻す + onIntersect(cr, interval); + if (!(mode instanceof ModeOnGround)) { + // 落ちてきてめり込んだ + // 6. 地面モードにする + mode = modeOnGround; + ((ModeOnGround) mode).setNormal(cr.normal); + onEndFall(); + } else { + // 歩いている途中で、アニメーションの関係で一瞬だけめり込んだ + // または、歩いている途中で斜面に差し掛かった + ((ModeOnGround) mode).setNormal(cr.normal); + } + } + } else if (cr.normal.dot(PhysicsUtility.vertical) >= -GeometryUtility.TOLERANCE) { + // 垂直壁にめり込んだ + // 5.1. 押し戻す + onIntersect(cr, interval); + } else { + // 下からぶつかった、または接触した(頭をぶつけた) + if (cr.length > GeometryUtility.TOLERANCE) { + // 5.1. 押し戻す + onIntersect(cr, interval); + } + } + cr = null; + } else { + // 地面とぶつかっても接触してもいない時 + // 6. 落下モードにする + mode = modeFreeFall; + } + } + + public void motion(long interval, Ground ground, ArrayList forces, + ArrayList appPoints) { + + forces.add(getGravity()); + appPoints.add(getGravityCenter()); + + // 1. 位置を動かす + ((Solid3D) body).move(interval, forces, appPoints); + + if (animation != null) { + // 2. アニメーションを実行 + if (animation.progress(interval) == false) { + onEndAnimation(); + } + + // 3. 姿勢を変える + body.apply(animation.getPose(), false); + } + + // 4. 衝突判定 + CollisionResult cr = PhysicsUtility.doesIntersect((Solid3D) body, ground); + + // 5. 衝突応答 + if (cr != null) { + // 構造物にぶつかった、または接触している時 + if (cr.normal.dot(PhysicsUtility.vertical) > GeometryUtility.TOLERANCE) { + // 上向きの面(=地面)にぶつかった、または接触している時 + if (cr.length <= GeometryUtility.TOLERANCE) { + // 地面に乗っている + if (!(mode instanceof ModeOnGround)) { + // 落ちてきて丁度乗った + mode = modeOnGround; + ((ModeOnGround) mode).setNormal(cr.normal); + onEndFall(); + } + } else { + // 地面にめり込んだ + // 5.1. 押し戻す + onIntersect(cr, interval); + if (!(mode instanceof ModeOnGround)) { + // 落ちてきてめり込んだ + // 6. 地面モードにする + mode = modeOnGround; + ((ModeOnGround) mode).setNormal(cr.normal); + onEndFall(); + } else { + // 歩いている途中で、アニメーションの関係で一瞬だけめり込んだ + // または、歩いている途中で斜面に差し掛かった + ((ModeOnGround) mode).setNormal(cr.normal); + } + } + } else if (cr.normal.dot(PhysicsUtility.vertical) >= -GeometryUtility.TOLERANCE) { + // 垂直壁にめり込んだ + // 5.1. 押し戻す + onIntersect(cr, interval); + } else { + // 下からぶつかった、または接触した(頭をぶつけた) + if (cr.length > GeometryUtility.TOLERANCE) { + // 5.1. 押し戻す + onIntersect(cr, interval); + } + } + cr = null; + } else { + // 地面とぶつかっても接触してもいない時 + // 6. 落下モードにする + mode = modeFreeFall; + } + } + + public void setInitialDirection(Vector3d dir) { + direction = dir; + } + + public Vector3d getInitialDirection() { + return direction; + } + + /** + * 指定した方向に向かせる + * @param vec 新しい向き + */ + public void setDirection(Vector3d vec) { + Vector3d v1 = new Vector3d(); + Vector3d v2 = new Vector3d(); + v1.cross(direction, GeometryUtility.Y_AXIS); + v2.cross(vec, GeometryUtility.Y_AXIS); + if (v2.length() < GeometryUtility.TOLERANCE) return; + v1.normalize(); + v2.normalize(); + double cos = v1.dot(v2); + v1.cross(v1, v2); + double sin = v1.dot(GeometryUtility.Y_AXIS); + double angle = Math.atan2(sin, cos); + AxisAngle4d axisAngle = new AxisAngle4d(GeometryUtility.Y_AXIS, angle); + Quaternion3D quat = new Quaternion3D(axisAngle); + ((Solid3D) body).apply(quat, false); + } + + /** + * 指定した方向に向かせる + * @param vec 新しい向き + */ + public void setDirection3D(Vector3d vec) { + Vector3d v1 = new Vector3d(); + Vector3d v2 = new Vector3d(); + v1.cross(direction, GeometryUtility.Y_AXIS); + double angle = Math.PI / 2.0 - Math.acos(vec.dot(GeometryUtility.Y_AXIS)); + AxisAngle4d axisAngle2 = new AxisAngle4d(v1, angle); + v2.cross(vec, GeometryUtility.Y_AXIS); + if (v2.length() < GeometryUtility.TOLERANCE) return; + v1.normalize(); + v2.normalize(); + double cos = v1.dot(v2); + v1.cross(v1, v2); + double sin = v1.dot(GeometryUtility.Y_AXIS); + angle = Math.atan2(sin, cos); + AxisAngle4d axisAngle = new AxisAngle4d(GeometryUtility.Y_AXIS, angle); + Quaternion3D quat = new Quaternion3D(axisAngle); + Quaternion3D quat2 = new Quaternion3D(axisAngle2); + quat.mul(quat2); + ((Solid3D) body).apply(quat, false); + } + + /** + * 現在向いている方向を取得する + * @return 現在の向き + */ + public Vector3d getDirection() { + Vector3d dir = new Vector3d(direction); + Transform3D trans = new Transform3D(); + trans.set(((Solid3D) body).getQuaternion().getAxisAngle()); + trans.transform(dir); + return dir; + } + + /** + * 移動速度ベクトルを設定する + * @param vel 新しい移動速度ベクトル + */ + public void setVelocity(Velocity3D vel) { + ((Solid3D) body).apply(vel, false); + } + + /** + * 移動速度ベクトルを取得する + * @return 現在の移動速度ベクトル + */ + public Velocity3D getVelocity() { + return ((Solid3D) body).getVelocity(); + } + + /** + * X軸を中心に回転する + * @param angle 回転角(反時計回り, 単位:ラジアン) + */ + public void rotX(double angle) { + Quaternion3D curQuat = body.getQuaternion(); + curQuat.add(new AxisAngle4d(GeometryUtility.X_AXIS, angle)); + body.apply(curQuat, false); + } + + /** + * Y軸を中心に回転する + * @param angle 回転角(反時計回り, 単位:ラジアン) + */ + public void rotY(double angle) { + Quaternion3D curQuat = body.getQuaternion(); + curQuat.add(new AxisAngle4d(GeometryUtility.Y_AXIS, angle)); + body.apply(curQuat, false); + } + + /** + * Z軸を中心に回転する + * @param angle 回転角(反時計回り, 単位:ラジアン) + */ + public void rotZ(double angle) { + Quaternion3D curQuat = body.getQuaternion(); + curQuat.add(new AxisAngle4d(GeometryUtility.Z_AXIS, angle)); + body.apply(curQuat, false); + } + + /** + * 加わっている重力を取得する + * @return 重力 + */ + public Force3D getGravity() { + return mode.getForce((Solid3D) body); + } + + /** + * 重心を取得する + * @return 重心位置 + */ + public Position3D getGravityCenter() { + return ((Solid3D) body).getGravityCenter(); + } + + /** + * 地面に乗っている状態か否かを取得する + * @return true --- 地面に乗っている, false --- 地面に乗っていない(空中にいる) + */ + public boolean isOnGround() { + return (mode instanceof ModeOnGround); + } + + /** + * 地面(構造物)に落下した瞬間に呼び出される + */ + public abstract void onEndFall(); + + /** + * 地面(構造物)に衝突した瞬間に呼び出される + * @param normal --- 地面の法線方向ベクトル + * @param interval --- 前回の動作からの経過時間(ミリ秒単位) + */ + public abstract void onIntersect(CollisionResult normal, long interval); + +} diff --git a/src/main/java/framework/gameMain/ActorModel.java b/src/main/java/framework/gameMain/ActorModel.java new file mode 100644 index 0000000..d61cbf4 --- /dev/null +++ b/src/main/java/framework/gameMain/ActorModel.java @@ -0,0 +1,14 @@ +package framework.gameMain; +import framework.physics.Solid3D; + + +public abstract class ActorModel extends GameModel { + + public ActorModel(String fileName) { + super(fileName); + } + + public Solid3D createBody() { + return new Solid3D(getModel().createObject()); + } +} diff --git a/src/main/java/framework/gameMain/Animatable.java b/src/main/java/framework/gameMain/Animatable.java new file mode 100644 index 0000000..47634f7 --- /dev/null +++ b/src/main/java/framework/gameMain/Animatable.java @@ -0,0 +1,54 @@ +package framework.gameMain; + +import framework.animation.Animation3D; +import framework.model3D.*; + +import javax.media.j3d.TransformGroup; + +public abstract class Animatable { + public Object3D body; + public Animation3D animation; + + public Animatable(Object3D body, Animation3D animation) { + this.body = body; + this.animation = animation; + } + + /** + * 単位時間ごとの動作(アニメーション処理) + * @param interval --- 前回呼び出されたときからの経過時間(ミリ秒単位) + */ + public void motion(long interval) { + if (animation != null) { + // 1. アニメーションを実行 + if (animation.progress(interval) == false) { + onEndAnimation(); + } + + // 2. 姿勢を変える + body.apply(animation.getPose(), false); + } + } + + public TransformGroup getTransformGroupToPlace() { + return getBody().getTransformGroupToPlace(); + } + + public BaseObject3D getBody() { + return body; + } + + /** + * アニメーションが終了するたびに呼ばれる + */ + public abstract void onEndAnimation(); + + public Position3D getPosition() { + return body.getPosition3D(); + } + + public void setPosition(Position3D p) { + body.apply(p, false); + } + +} \ No newline at end of file diff --git a/src/main/java/framework/gameMain/BaseGame.java b/src/main/java/framework/gameMain/BaseGame.java new file mode 100644 index 0000000..30bd79f --- /dev/null +++ b/src/main/java/framework/gameMain/BaseGame.java @@ -0,0 +1,48 @@ +package framework.gameMain; + +import java.util.Stack; + +/** + * 一般的なゲーム用のクラス(状態遷移を持つことができる) + * @author 新田直也 + * + */ +public abstract class BaseGame extends AbstractGame { + Stack gameStateStack = new Stack(); + + public abstract AbstractGameState getInitialGameState(); + public abstract boolean canGoPrevGameState(); + public abstract AbstractGameState changeNextGameState(); + + public BaseGame() { + super(); + pushNewGameState(getInitialGameState()); + } + + protected AbstractGameState getCurrentGameState() { + return gameStateStack.peek(); + } + + public void goNextGameState(){ + deactivateState(getCurrentGameState()); + AbstractGameState g = changeNextGameState(); + pushNewGameState(g); + activateState(g); + } + + public void goPrevGameState(){ + if(canGoPrevGameState()){ + deactivateState(getCurrentGameState()); + popGameState(); + activateState(getCurrentGameState()); + } + } + + private void pushNewGameState(AbstractGameState g) { + gameStateStack.push(g); + } + + private void popGameState() { + gameStateStack.pop(); + } +} diff --git a/src/main/java/framework/gameMain/BaseScenarioGameContainer.java b/src/main/java/framework/gameMain/BaseScenarioGameContainer.java new file mode 100644 index 0000000..8be84ba --- /dev/null +++ b/src/main/java/framework/gameMain/BaseScenarioGameContainer.java @@ -0,0 +1,52 @@ +package framework.gameMain; + +import framework.RWT.RWTCanvas3D; +import framework.RWT.RWTContainer; +import framework.RWT.RWTLabel; +import framework.scenario.ScenarioManager; + +import java.awt.*; + +/** + * シナリオゲーム用画面 + * @author Nitta + * + */ +abstract public class BaseScenarioGameContainer extends RWTContainer { + protected RWTCanvas3D canvas; + protected RWTLabel dialog; + protected ScenarioManager scenario; + + public BaseScenarioGameContainer(ScenarioManager scenario) { + this.scenario = scenario; + } + + @Override + public void build(GraphicsConfiguration gc) { + if (gc != null) { + canvas = new RWTCanvas3D(gc); + } else { + canvas = new RWTCanvas3D(); + } + dialog = new RWTLabel(); + } + + public void dialogOpen() { + dialog.setVisible(true); + repaint(); + } + + public void dialogClose() { + dialog.setVisible(false); + repaint(); + } + + public void dialogMessage(String message) { + dialog.setString(message); + repaint(); + } + + public boolean isDialogOpen() { + return dialog.isVisible(); + } +} diff --git a/src/main/java/framework/gameMain/GameModel.java b/src/main/java/framework/gameMain/GameModel.java new file mode 100644 index 0000000..4eeef16 --- /dev/null +++ b/src/main/java/framework/gameMain/GameModel.java @@ -0,0 +1,29 @@ +package framework.gameMain; +import framework.animation.Animation3D; +import framework.model3D.Model3D; +import framework.model3D.ModelFactory; + +public class GameModel { + + private Model3D model = null; + private String fileName; + + public GameModel(String fileName) { + this.fileName = fileName; + } + + public Animation3D getAnimation() { + return new Animation3D(); + } + + public Model3D getModel() { + if(model == null && fileName != null) { + model = ModelFactory.loadModel(fileName, false, true); + } + return model; + } + + public void clearModel(){ + model = null; + } +} \ No newline at end of file diff --git a/src/main/java/framework/gameMain/IGameState.java b/src/main/java/framework/gameMain/IGameState.java new file mode 100644 index 0000000..a00cdc9 --- /dev/null +++ b/src/main/java/framework/gameMain/IGameState.java @@ -0,0 +1,9 @@ +package framework.gameMain; +import framework.RWT.RWTFrame3D; +import framework.RWT.RWTVirtualController; + +public interface IGameState { + public abstract void init(RWTFrame3D frame); + public abstract boolean useTimer(); + public abstract void update(RWTVirtualController virtualController, long interval); +} \ No newline at end of file diff --git a/src/main/java/framework/gameMain/Mode.java b/src/main/java/framework/gameMain/Mode.java new file mode 100644 index 0000000..a8a02f1 --- /dev/null +++ b/src/main/java/framework/gameMain/Mode.java @@ -0,0 +1,10 @@ +package framework.gameMain; +import framework.physics.Force3D; +import framework.physics.Solid3D; + + +public abstract class Mode { + + abstract Force3D getForce(Solid3D body); + +} diff --git a/src/main/java/framework/gameMain/ModeFreeFall.java b/src/main/java/framework/gameMain/ModeFreeFall.java new file mode 100644 index 0000000..cecb897 --- /dev/null +++ b/src/main/java/framework/gameMain/ModeFreeFall.java @@ -0,0 +1,11 @@ +package framework.gameMain; +import framework.physics.Force3D; +import framework.physics.PhysicsUtility; +import framework.physics.Solid3D; + + +public class ModeFreeFall extends Mode { + Force3D getForce(Solid3D body) { + return PhysicsUtility.getGravity(body); + } +} diff --git a/src/main/java/framework/gameMain/ModeOnGround.java b/src/main/java/framework/gameMain/ModeOnGround.java new file mode 100644 index 0000000..6ded856 --- /dev/null +++ b/src/main/java/framework/gameMain/ModeOnGround.java @@ -0,0 +1,24 @@ +package framework.gameMain; + +import framework.physics.Force3D; +import framework.physics.PhysicsUtility; +import framework.physics.Solid3D; + +import javax.vecmath.Vector3d; + + +public class ModeOnGround extends Mode { + private Vector3d normal = PhysicsUtility.vertical; + + public Force3D getForce(Solid3D body) { + return Force3D.ZERO; + } + + public void setNormal(Vector3d normal) { + this.normal = normal; + } + + public Vector3d getNormal() { + return normal; + } +} diff --git a/src/main/java/framework/gameMain/MultiViewGame.java b/src/main/java/framework/gameMain/MultiViewGame.java new file mode 100644 index 0000000..8aca91e --- /dev/null +++ b/src/main/java/framework/gameMain/MultiViewGame.java @@ -0,0 +1,92 @@ +package framework.gameMain; + +import framework.RWT.*; +import framework.model3D.Universe; +import framework.view3D.Camera3D; + +import javax.media.j3d.GraphicsConfigTemplate3D; +import java.awt.*; + +/** + * 状態遷移のないゲーム用のクラス + * @author 新田直也 + * + */ +public abstract class MultiViewGame extends AbstractGame implements IGameState { + protected Universe universe; + protected Camera3D camera1; + protected Camera3D camera2; + protected RWTContainer container; + + public abstract void init(Universe universe, Camera3D camera1, Camera3D camera2); + public abstract void progress(RWTVirtualController virtualController, long interval); + + @Override + protected IGameState getCurrentGameState() { + return this; + } + + @Override + public boolean useTimer() { + return true; + } + + @Override + public void init(RWTFrame3D frame) { + container = new RWTContainer() { + @Override + public void build(GraphicsConfiguration gc) { + RWTCanvas3D canvas1, canvas2; + if (gc != null) { + canvas1 = new RWTCanvas3D(gc); + canvas2 = new RWTCanvas3D(gc); + } else { + canvas1 = new RWTCanvas3D(); + canvas2 = new RWTCanvas3D(); + } + canvas1.setRelativePosition(0.0f, 0.0f); + canvas1.setRelativeSize(0.5f, 1.0f); + canvas2.setRelativePosition(0.5f, 0.0f); + canvas2.setRelativeSize(0.5f, 1.0f); + addCanvas(canvas1); + addCanvas(canvas2); + + repaint(); + } + // RWT側ではイベント処理をしない + @Override + public void keyPressed(RWTVirtualKey key) {} + @Override + public void keyReleased(RWTVirtualKey key) {} + @Override + public void keyTyped(RWTVirtualKey key) {} + }; + frame.setContentPane(container); + GraphicsConfiguration gc = null; + if (frame.isShadowCasting()) { + // 影を付ける場合 + // ステンシルバッファを使用する GraphicsConfiguration の生成 + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice gd = ge.getDefaultScreenDevice(); + GraphicsConfigTemplate3D gct3D = new GraphicsConfigTemplate3D(); + gct3D.setStencilSize(8); + gc = gd.getBestConfiguration(gct3D); + } + container.build(gc); + + universe = new Universe(); + camera1 = new Camera3D(universe); + camera2 = new Camera3D(universe); + init(universe, camera1, camera2); + container.getRWTCanvas3D(0).attachCamera(camera1); + container.getRWTCanvas3D(1).attachCamera(camera2); + universe.compile(); + } + + @Override + public void update(RWTVirtualController virtualController, long interval) { + progress(virtualController, interval); + camera1.adjust(interval); + camera2.adjust(interval); + } +} diff --git a/src/main/java/framework/gameMain/OvergroundActor.java b/src/main/java/framework/gameMain/OvergroundActor.java new file mode 100644 index 0000000..5ffc222 --- /dev/null +++ b/src/main/java/framework/gameMain/OvergroundActor.java @@ -0,0 +1,51 @@ +package framework.gameMain; + +import framework.animation.Animation3D; +import framework.model3D.CollisionResult; +import framework.model3D.Object3D; +import framework.model3D.Position3D; +import framework.physics.Solid3D; +import framework.physics.Velocity3D; + +import javax.vecmath.Vector3d; + +/** + * 地面の上を移動するもの(ジャンプや自由落下させることも可能) + * @author 新田直也 + * + */ +public class OvergroundActor extends Actor { + + public OvergroundActor(Object3D body, Animation3D animation) { + super(body, animation); + } + + public OvergroundActor(Solid3D body, Animation3D animation) { + super(body, animation); + } + + public OvergroundActor(ActorModel model) { + super(model); + } + + public void onIntersect(CollisionResult cr, long interval) { + // めり込んだら(めり込んだ面の法線方向に)押し戻す + Vector3d back = (Vector3d) cr.normal.clone(); + back.scale(cr.length * 2.0); + body.apply(new Position3D(body.getPosition3D().add(back)), false); + + // 速度の面の法線方向の成分を 0 にする + Vector3d v = (Vector3d) ((Solid3D)body).getVelocity().getVector3d().clone(); + double d = v.dot(cr.normal); + v.scaleAdd(-d, cr.normal, v); + body.apply(new Velocity3D(v), false); + } + + @Override + public void onEndFall() { + } + + @Override + public void onEndAnimation() { + } +} \ No newline at end of file diff --git a/src/main/java/framework/gameMain/SimpleGame.java b/src/main/java/framework/gameMain/SimpleGame.java new file mode 100644 index 0000000..9284a28 --- /dev/null +++ b/src/main/java/framework/gameMain/SimpleGame.java @@ -0,0 +1,93 @@ +package framework.gameMain; + +import framework.RWT.*; +import framework.model3D.Universe; +import framework.view3D.Camera3D; + +import javax.media.j3d.GraphicsConfigTemplate3D; +import java.awt.*; + +/** + * 状態遷移のないゲーム用のクラス + * @author 新田直也 + * + */ +public abstract class SimpleGame extends AbstractGame implements IGameState { + protected Universe universe; + protected Camera3D camera; + private IGameState currentState = this; + + public abstract void init(Universe universe, Camera3D camera); + public abstract void progress(RWTVirtualController virtualController, long interval); + + @Override + protected IGameState getCurrentGameState() { + return currentState; + } + + protected void setCurrentGameState(IGameState state) { + currentState = state; + } + + @Override + public boolean useTimer() { + return true; + } + + @Override + public void init(RWTFrame3D frame) { + RWTContainer container = createRWTContainer(); + frame.setContentPane(container); + GraphicsConfiguration gc = null; + if (frame.isShadowCasting()) { + // 影を付ける場合 + // ステンシルバッファを使用する GraphicsConfiguration の生成 + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice gd = ge.getDefaultScreenDevice(); + GraphicsConfigTemplate3D gct3D = new GraphicsConfigTemplate3D(); + gct3D.setStencilSize(8); + gc = gd.getBestConfiguration(gct3D); + } + container.build(gc); + + universe = new Universe(); + camera = new Camera3D(universe); +// universe.setCamera(camera); +// SceneGraph root = new SceneGraph(); +// universe.setRoot(root); + init(universe, camera); + container.getPrimaryRWTCanvas3D().attachCamera(camera); + universe.compile(); + } + + @Override + public void update(RWTVirtualController virtualController, long interval) { + progress(virtualController, interval); + camera.adjust(interval); + } + + protected RWTContainer createRWTContainer() { + return new RWTContainer() { + @Override + public void build(GraphicsConfiguration gc) { + RWTCanvas3D canvas; + if (gc != null) { + canvas = new RWTCanvas3D(gc); + } else { + canvas = new RWTCanvas3D(); + } + canvas.setRelativePosition(0.0f, 0.0f); + canvas.setRelativeSize(1.0f, 1.0f); + addCanvas(canvas); + repaint(); + } + // RWT側ではイベント処理をしない + @Override + public void keyPressed(RWTVirtualKey key) {} + @Override + public void keyReleased(RWTVirtualKey key) {} + @Override + public void keyTyped(RWTVirtualKey key) {} + }; + } +} diff --git a/src/main/java/framework/gameMain/SimpleScenarioGame.java b/src/main/java/framework/gameMain/SimpleScenarioGame.java new file mode 100644 index 0000000..b447e44 --- /dev/null +++ b/src/main/java/framework/gameMain/SimpleScenarioGame.java @@ -0,0 +1,42 @@ +package framework.gameMain; + +import framework.scenario.IWorld; +import framework.scenario.ScenarioManager; + +/** + * シナリオ駆動型ゲームのためのテンプレート + * @author 新田直也 + * + */ +abstract public class SimpleScenarioGame extends SimpleGame implements IWorld { + protected BaseScenarioGameContainer container; + protected ScenarioManager scenario; + + @Override + public void dialogOpen() { + container.dialogOpen(); + } + + @Override + public void dialogClose() { + container.dialogClose(); + } + + @Override + public void dialogMessage(String message) { + container.dialogMessage(message); + } + + @Override + public void showOption(int n, String option) { + } + + @Override + public boolean isDialogOpen() { + return container.isDialogOpen(); + } + + public void setScenario(String scenarioFile) { + scenario = new ScenarioManager(scenarioFile, this); + } +} diff --git a/src/main/java/framework/gameMain/Water.java b/src/main/java/framework/gameMain/Water.java new file mode 100644 index 0000000..cba417b --- /dev/null +++ b/src/main/java/framework/gameMain/Water.java @@ -0,0 +1,193 @@ +package framework.gameMain; + +import com.sun.j3d.utils.image.TextureLoader; +import framework.animation.Animation3D; +import framework.animation.PartAnimation; +import framework.model3D.BumpMapGenerator; +import framework.model3D.FresnelsReflectionMapGenerator; +import framework.model3D.Object3D; +import framework.model3D.Position3D; + +import javax.media.j3d.*; +import javax.vecmath.*; + +/** + * 水面を表現するオブジェクト。 + * 波の動きはバンプマッピングをスクロールさせて表現している。 + * 反射ではフレネル効果を加味している。 + * Java3Dでは、頂点間の反射ベクトルは線形補間でしか求めていないため、 + * 精度を上げるために、LODを使って水面の視点に近い部分はメッシュ分割を細かくしている。 + * @author Nitta + * + */ +public class Water extends Animatable { + private static int DEFAULT_FIRST_LEVEL_MESH_SIZE = 25; + private static int DEFAULT_SECOND_LEVEL_MESH_SIZE = 1; + private static int DEFAULT_THIRD_LEVEL_MESH_SIZE = 40; + + public Water(double minX, double minZ, double maxX, double maxZ, double height, boolean bUseLOD) { + this(minX, minZ, maxX, maxZ, height, 0.5f, bUseLOD); + } + + public Water(double minX, double minZ, double maxX, double maxZ, double height, float transparency, boolean bUseLOD) { + this(minX, minZ, maxX, maxZ, height, transparency, 0.3f, bUseLOD); + } + + public Water(double minX, double minZ, double maxX, double maxZ, double height, float transparency, float reflection, boolean bUseLOD) { + this(minX, minZ, maxX, maxZ, height, transparency, reflection, new Color3f(0.2f, 0.5f, 1.0f), bUseLOD); + } + + public Water(double minX, double minZ, double maxX, double maxZ, double height, + float transparency, float reflection, Color3f waterColor, boolean bUseLOD) { + this(minX, minZ, maxX, maxZ, height, transparency, reflection, new Color3f(0.2f, 0.5f, 1.0f), + DEFAULT_FIRST_LEVEL_MESH_SIZE, DEFAULT_SECOND_LEVEL_MESH_SIZE, DEFAULT_THIRD_LEVEL_MESH_SIZE, bUseLOD); + } + + public Water(double minX, double minZ, double maxX, double maxZ, double height, float transparency, float reflection, + int firstLevelMeshSize, int secondLevelMeshSize, int thirdLevelMeshSize, boolean bUseLOD) { + this(minX, minZ, maxX, maxZ, height, transparency, reflection, new Color3f(0.2f, 0.5f, 1.0f), + firstLevelMeshSize, secondLevelMeshSize, thirdLevelMeshSize, bUseLOD); + } + + public Water(double minX, double minZ, double maxX, double maxZ, double height, float transparency, float reflection, Color3f waterColor, + int firstLevelMeshSize, int secondLevelMeshSize, int thirdLevelMeshSize, boolean bUseLOD) { + super(null, null); + + int firstLevelMeshSizeX = firstLevelMeshSize; + int firstLevelMeshSizeZ = firstLevelMeshSize; + int secondLevelMeshSizeX = secondLevelMeshSize; + int secondLevelMeshSizeZ = secondLevelMeshSize; + int thirdLevelMeshSizeX = thirdLevelMeshSize; + int thirdLevelMeshSizeZ = thirdLevelMeshSize; + + + // 水面のジオメトリの作成 + IndexedQuadArray coarseWaterGeometry = + new IndexedQuadArray((secondLevelMeshSizeX + 1) * (secondLevelMeshSizeZ + 1), + IndexedQuadArray.COORDINATES | IndexedQuadArray.NORMALS, 4 * secondLevelMeshSizeX * secondLevelMeshSizeZ); + + IndexedQuadArray fineWaterGeometry = + new IndexedQuadArray((secondLevelMeshSizeX * thirdLevelMeshSizeX + 1) * (secondLevelMeshSizeZ * thirdLevelMeshSizeZ + 1), + IndexedQuadArray.COORDINATES | IndexedQuadArray.NORMALS, 4 * secondLevelMeshSizeX * thirdLevelMeshSizeX * secondLevelMeshSizeZ * thirdLevelMeshSizeZ); + + double unitSizeX1 = (maxX - minX) / firstLevelMeshSizeX; + double unitSizeZ1 = (maxZ - minZ) / firstLevelMeshSizeZ; + double unitSizeX2 = unitSizeX1 / secondLevelMeshSizeX; + double unitSizeZ2 = unitSizeZ1 / secondLevelMeshSizeZ; + double unitSizeX3 = unitSizeX2 / thirdLevelMeshSizeX; + double unitSizeZ3 = unitSizeZ2 / thirdLevelMeshSizeZ; + double baseX = -(maxX - minX) / firstLevelMeshSizeX / 2.0; + double baseZ = -(maxZ - minZ) / firstLevelMeshSizeZ / 2.0; + + // 第二、第三レベルは LOD で切り替え + for (int z2 = 0; z2 < secondLevelMeshSizeZ + 1; z2++) { + for (int x2 = 0; x2 < secondLevelMeshSizeX + 1; x2++) { + int indexX2 = x2; + int indexZ2 = z2; + int index20 = indexZ2 * secondLevelMeshSizeX + indexX2; + int index21 = indexZ2 * (secondLevelMeshSizeX + 1) + indexX2; + int index22 = (indexZ2 + 1) * (secondLevelMeshSizeX + 1) + indexX2; + double xx2 = baseX + unitSizeX2 * indexX2; + double zz2 = baseZ + unitSizeZ2 * indexZ2; + coarseWaterGeometry.setCoordinate(index21, new Point3d(xx2, 0.0, zz2)); + coarseWaterGeometry.setNormal(index21, new Vector3f(0.0f, 1.0f, 0.0f)); + if (indexX2 < secondLevelMeshSizeX && indexZ2 < secondLevelMeshSizeZ) { + coarseWaterGeometry.setCoordinateIndices(index20 * 4, new int[]{index22, index22 + 1, index21 + 1, index21}); + if (bUseLOD) { + for (int z3 = 0; z3 < thirdLevelMeshSizeZ + 1; z3++) { + for (int x3 = 0; x3 < thirdLevelMeshSizeX + 1; x3++) { + int indexX3 = x2 * thirdLevelMeshSizeX + x3; + int indexZ3 = z2 * thirdLevelMeshSizeZ + z3; + int index30 = indexZ3 * (secondLevelMeshSizeX * thirdLevelMeshSizeX) + indexX3; + int index31 = indexZ3 * (secondLevelMeshSizeX * thirdLevelMeshSizeX + 1) + indexX3; + int index32 = (indexZ3 + 1) * (secondLevelMeshSizeX * thirdLevelMeshSizeX + 1) + indexX3; + double xx3 = baseX + unitSizeX3 * indexX3; + double zz3 = baseZ + unitSizeZ3 * indexZ3; + fineWaterGeometry.setCoordinate(index31, new Point3d(xx3, 0.0, zz3)); + fineWaterGeometry.setNormal(index31, new Vector3f(0.0f, 1.0f, 0.0f)); + if (indexX3 < secondLevelMeshSizeX * thirdLevelMeshSizeX + && indexZ3 < secondLevelMeshSizeZ * thirdLevelMeshSizeZ) { + fineWaterGeometry.setCoordinateIndices(index30 * 4, new int[]{index32, index32 + 1, index31 + 1, index31}); + } + } + } + } + } + } + } + + // 第一レベルは Object3D の集合で構成 + Object3D children[] = new Object3D[firstLevelMeshSizeX * firstLevelMeshSizeZ]; + TextureLoader loaderWater = new TextureLoader("data\\texture\\waternormalMap.jpg", + TextureLoader.BY_REFERENCE, + null); + Texture textureWater = loaderWater.getTexture(); + // 水面の表面属性の作成(全体で共有する) + Appearance a = new Appearance(); + a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_WRITE); + Material m1 = new Material(); + m1.setDiffuseColor(waterColor); // 水の色 + m1.setSpecularColor(0.0f, 0.0f, 0.0f); + a.setMaterial(m1); + ColoringAttributes ca = new ColoringAttributes(); + ca.setShadeModel(ColoringAttributes.NICEST); + a.setColoringAttributes(ca); + + TransparencyAttributes ta = new TransparencyAttributes(); + ta.setTransparency(transparency); // 透明度 + ta.setTransparencyMode(TransparencyAttributes.BLENDED); + a.setTransparencyAttributes(ta); + // 拡張表面属性(全体で共有する) + TexCoordGeneration tcg = new TexCoordGeneration(TexCoordGeneration.OBJECT_LINEAR, TexCoordGeneration.TEXTURE_COORDINATE_3); + tcg.setPlaneS(new Vector4f(0.1f, 0.0f, 0.0f, 0.0f)); + tcg.setPlaneT(new Vector4f(0.0f, 0.0f, 0.1f, 0.0f)); + BumpMapGenerator bumpMapGenerator = new BumpMapGenerator(textureWater, tcg, true); // テクスチャユニットを1つ使用 + FresnelsReflectionMapGenerator fresnelsReflectionMapGenerator = + new FresnelsReflectionMapGenerator(new Color4f(reflection, reflection, reflection, 1.0f), true); // テクスチャユニットを2つ使用 + + for (int z1 = 0; z1 < firstLevelMeshSizeZ; z1++) { + for (int x1 = 0; x1 < firstLevelMeshSizeX; x1++) { + double xx1 = minX + (maxX - minX) / firstLevelMeshSizeX * x1; + double zz1 = minZ + (maxZ - minZ) / firstLevelMeshSizeZ * z1; + Object3D waterSurfaceUnit; + if (bUseLOD) { + DistanceLOD lod = new DistanceLOD(); + Switch surfaceSwitch = new Switch(); + surfaceSwitch.addChild(new Shape3D(fineWaterGeometry, a)); + surfaceSwitch.addChild(new Shape3D(coarseWaterGeometry, a)); + lod.addSwitch(surfaceSwitch); + lod.setDistance(0, unitSizeX1 / 2.0); + lod.setPosition(new Point3f(0.0f, 0.0f, 0.0f)); + waterSurfaceUnit = new Object3D("waterUnit", lod); + } else { + waterSurfaceUnit = new Object3D("waterUnit", new Shape3D(coarseWaterGeometry, a)); + } + waterSurfaceUnit.apply(new Position3D(xx1, height, zz1), false); + + // 拡張表面属性を設定 + waterSurfaceUnit.setBumpMapping(bumpMapGenerator); + waterSurfaceUnit.setReflectionMapping(fresnelsReflectionMapGenerator); + + children[z1 * firstLevelMeshSizeX + x1] = waterSurfaceUnit; + } + } + + // 全体オブジェクトを構成 + Object3D waterSurface = new Object3D("water", children); + + body = waterSurface; + + // アニメーションの作成 + Animation3D waveAnimation = new Animation3D(); + PartAnimation pa = new PartAnimation("waterUnit", 0); + pa.addTexture(0L, new Position3D(0.0, 0.0, 0.0)); + pa.addTexture(10000L, new Position3D(1.0, -1.0, 0.0)); + waveAnimation.addPartAnimation(pa); + animation = waveAnimation; + } + + @Override + public void onEndAnimation() { + } +} diff --git a/src/main/java/framework/model3D/BackgroundBox.java b/src/main/java/framework/model3D/BackgroundBox.java new file mode 100644 index 0000000..e350486 --- /dev/null +++ b/src/main/java/framework/model3D/BackgroundBox.java @@ -0,0 +1,148 @@ +package framework.model3D; + +import javax.media.j3d.*; +import javax.vecmath.Point3d; +import javax.vecmath.TexCoord2f; + + +public class BackgroundBox extends Background { + Texture northTex; + Texture westTex; + Texture southTex; + Texture eastTex; + Texture topTex; + Texture bottomTex; + + public BackgroundBox(Texture northTex, Texture westTex, Texture southTex, Texture eastTex, Texture topTex, Texture bottomTex) { + this.northTex = northTex; + this.westTex = westTex; + this.southTex = southTex; + this.eastTex = eastTex; + this.topTex = topTex; + this.bottomTex = bottomTex; + Point3d p0 = new Point3d(-1.0,-1.0,-1.0); + Point3d p1 = new Point3d(-1.0,-1.0, 1.0); + Point3d p2 = new Point3d(-1.0, 1.0,-1.0); + Point3d p3 = new Point3d(-1.0, 1.0, 1.0); + Point3d p4 = new Point3d( 1.0, 1.0,-1.0); + Point3d p5 = new Point3d( 1.0, 1.0, 1.0); + Point3d p6 = new Point3d( 1.0,-1.0,-1.0); + Point3d p7 = new Point3d( 1.0,-1.0, 1.0); + QuadArray top = new QuadArray(4, QuadArray.COORDINATES | QuadArray.TEXTURE_COORDINATE_2); + top.setCoordinate(0, p4); + top.setCoordinate(1, p5); + top.setCoordinate(2, p3); + top.setCoordinate(3, p2); + top.setTextureCoordinate(0, 0, new TexCoord2f(1.0f, 0.0f)); + top.setTextureCoordinate(0, 1, new TexCoord2f(1.0f, 1.0f)); + top.setTextureCoordinate(0, 2, new TexCoord2f(0.0f, 1.0f)); + top.setTextureCoordinate(0, 3, new TexCoord2f(0.0f, 0.0f)); + QuadArray bottom = new QuadArray(4, QuadArray.COORDINATES | QuadArray.TEXTURE_COORDINATE_2); + bottom.setCoordinate(0, p7); + bottom.setCoordinate(1, p6); + bottom.setCoordinate(2, p0); + bottom.setCoordinate(3, p1); + bottom.setTextureCoordinate(0, 0, new TexCoord2f(1.0f, 0.0f)); + bottom.setTextureCoordinate(0, 1, new TexCoord2f(1.0f, 1.0f)); + bottom.setTextureCoordinate(0, 2, new TexCoord2f(0.0f, 1.0f)); + bottom.setTextureCoordinate(0, 3, new TexCoord2f(0.0f, 0.0f)); + QuadArray north = new QuadArray(4, QuadArray.COORDINATES | QuadArray.TEXTURE_COORDINATE_2); + north.setCoordinate(0, p4); + north.setCoordinate(1, p6); + north.setCoordinate(2, p7); + north.setCoordinate(3, p5); + north.setTextureCoordinate(0, 0, new TexCoord2f(1.0f, 0.0f)); + north.setTextureCoordinate(0, 1, new TexCoord2f(1.0f, 1.0f)); + north.setTextureCoordinate(0, 2, new TexCoord2f(0.0f, 1.0f)); + north.setTextureCoordinate(0, 3, new TexCoord2f(0.0f, 0.0f)); + QuadArray south = new QuadArray(4, QuadArray.COORDINATES | QuadArray.TEXTURE_COORDINATE_2); + south.setCoordinate(0, p3); + south.setCoordinate(1, p1); + south.setCoordinate(2, p0); + south.setCoordinate(3, p2); + south.setTextureCoordinate(0, 0, new TexCoord2f(1.0f, 0.0f)); + south.setTextureCoordinate(0, 1, new TexCoord2f(1.0f, 1.0f)); + south.setTextureCoordinate(0, 2, new TexCoord2f(0.0f, 1.0f)); + south.setTextureCoordinate(0, 3, new TexCoord2f(0.0f, 0.0f)); + QuadArray east = new QuadArray(4, QuadArray.COORDINATES | QuadArray.TEXTURE_COORDINATE_2); + east.setCoordinate(0, p2); + east.setCoordinate(1, p0); + east.setCoordinate(2, p6); + east.setCoordinate(3, p4); + east.setTextureCoordinate(0, 0, new TexCoord2f(1.0f, 0.0f)); + east.setTextureCoordinate(0, 1, new TexCoord2f(1.0f, 1.0f)); + east.setTextureCoordinate(0, 2, new TexCoord2f(0.0f, 1.0f)); + east.setTextureCoordinate(0, 3, new TexCoord2f(0.0f, 0.0f)); + QuadArray west = new QuadArray(4, QuadArray.COORDINATES | QuadArray.TEXTURE_COORDINATE_2); + west.setCoordinate(0, p5); + west.setCoordinate(1, p7); + west.setCoordinate(2, p1); + west.setCoordinate(3, p3); + west.setTextureCoordinate(0, 0, new TexCoord2f(1.0f, 0.0f)); + west.setTextureCoordinate(0, 1, new TexCoord2f(1.0f, 1.0f)); + west.setTextureCoordinate(0, 2, new TexCoord2f(0.0f, 1.0f)); + west.setTextureCoordinate(0, 3, new TexCoord2f(0.0f, 0.0f)); + Appearance apTop = new Appearance(); + apTop.setTexture(topTex); + Shape3D shapeTop = new Shape3D(top, apTop); + + Appearance apBottom = new Appearance(); + apBottom.setTexture(bottomTex); + Shape3D shapeBottom = new Shape3D(bottom, apBottom); + + Appearance apNorth = new Appearance(); + apNorth.setTexture(northTex); + Shape3D shapeNorth = new Shape3D(north, apNorth); + + Appearance apSouth = new Appearance(); + apSouth.setTexture(southTex); + Shape3D shapeSouth = new Shape3D(south, apSouth); + + Appearance apWest = new Appearance(); + apWest.setTexture(westTex); + Shape3D shapeWest = new Shape3D(west, apWest); + + Appearance apEast = new Appearance(); + apEast.setTexture(eastTex); + Shape3D shapeEast = new Shape3D(east, apEast); + + BranchGroup bg = new BranchGroup(); + bg.addChild(shapeTop); + bg.addChild(shapeBottom); + bg.addChild(shapeNorth); + bg.addChild(shapeSouth); + bg.addChild(shapeWest); + bg.addChild(shapeEast); + + setGeometry(bg); + } + + public ImageComponent2D getNorthImage() { + return (ImageComponent2D)northTex.getImage(0); + } + + + public ImageComponent2D getSouthImage() { + return (ImageComponent2D)southTex.getImage(0); + } + + + public ImageComponent2D getEastImage() { + return (ImageComponent2D)eastTex.getImage(0); + } + + + public ImageComponent2D getWestImage() { + return (ImageComponent2D)westTex.getImage(0); + } + + + public ImageComponent2D getTopImage() { + return (ImageComponent2D)topTex.getImage(0); + } + + + public ImageComponent2D getBottomImage() { + return (ImageComponent2D)bottomTex.getImage(0); + } +} diff --git a/src/main/java/framework/model3D/BaseObject3D.java b/src/main/java/framework/model3D/BaseObject3D.java new file mode 100644 index 0000000..2d47dc3 --- /dev/null +++ b/src/main/java/framework/model3D/BaseObject3D.java @@ -0,0 +1,471 @@ +package framework.model3D; + +import com.sun.j3d.utils.geometry.*; + +import javax.media.j3d.*; +import javax.vecmath.Vector3d; +import javax.vecmath.Vector4d; +import java.util.ArrayList; +import java.util.Enumeration; + +public class BaseObject3D implements Placeable { + public TransformGroup center; + + protected BoundingSurface[] boundingSurfaces = null; + private ArrayList shadowVolumes = new ArrayList(); + + private BumpMapGenerator bumpMapGenerator = null; + private ReflectionMapGenerator reflectionMapGenerator = null; + protected boolean bBumpMapApplied = false; + protected boolean bReflectionMapApplied = false; + + public BaseObject3D() { + center = new TransformGroup(); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + } + + public BaseObject3D(Geometry g, Appearance a) { + init(g, a); + } + + // コピーコンストラクタ + public BaseObject3D(BaseObject3D obj) { + Transform3D transCenter = new Transform3D(); + obj.center.getTransform(transCenter); + this.center = new TransformGroup(transCenter); + this.center.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + this.center.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + Enumeration nodes = obj.getPrimitiveNodes(); + for (; nodes.hasMoreElements();) { + Node node = nodes.nextElement(); + if (node != null && node instanceof Shape3D) { + Shape3D shape = (Shape3D)node; + shape.setCapability(Shape3D.ALLOW_APPEARANCE_READ); + shape.setCapability(Shape3D.ALLOW_APPEARANCE_WRITE); + Appearance a = (Appearance)shape.getAppearance().cloneNodeComponent(true); + a.setCapability(Appearance.ALLOW_TEXTURE_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_WRITE); + a.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_WRITE); + a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_WRITE); + this.center.addChild(new Shape3D((Geometry) shape + .getAllGeometries().nextElement(), a)); + } else if (node != null && node instanceof Primitive) { + Primitive primitive = (Primitive)node; + primitive = (Primitive)primitive.cloneTree(); + primitive.setCapability(Primitive.ENABLE_APPEARANCE_MODIFY); + this.center.addChild(primitive); + } + } + if (obj.boundingSurfaces != null) { + this.boundingSurfaces = obj.boundingSurfaces; + } + this.bumpMapGenerator = obj.bumpMapGenerator; + this.reflectionMapGenerator = obj.reflectionMapGenerator; + this.bBumpMapApplied = obj.bBumpMapApplied; + this.bReflectionMapApplied = obj.bReflectionMapApplied; + } + + // 自分を複製する(クローンを作る) + public BaseObject3D duplicate() { + BaseObject3D obj = new BaseObject3D(this); + return obj; + } + + public void init(Geometry g, Appearance a) { + center = new TransformGroup(); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + a.setCapability(Appearance.ALLOW_TEXTURE_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_WRITE); + a.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_WRITE); + a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_WRITE); + Shape3D shape = new Shape3D(g, a); + shape.setCapability(Shape3D.ALLOW_APPEARANCE_READ); + shape.setCapability(Shape3D.ALLOW_APPEARANCE_WRITE); + shape.setCapability(Shape3D.ALLOW_GEOMETRY_READ); + shape.setCapability(Shape3D.ALLOW_GEOMETRY_WRITE); + center.addChild(shape); + } + + public void updateGeometry(Geometry g) { + Node node = getPrimitiveNode(); + if (node != null && node instanceof Shape3D) { + ((Shape3D)node).removeAllGeometries(); + ((Shape3D)node).addGeometry(g); + boundingSurfaces = null; + } + } + + @Override + public TransformGroup getTransformGroupToPlace() { + return getBody().center; + } + + public BaseObject3D getBody() { + return this; + } + +// public void placeFirst(RWTUniverse universe) { +// universe.getRoot().insertChild(this.center, 0); +// } +// +// public void placeLast(RWTUniverse universe) { +// universe.getRoot().addChild(this.center); +// } +// + public Node getPrimitiveNode() { + return (Node)center.getChild(0); + } + + public Enumeration getPrimitiveNodes() { + return (Enumeration)center.getAllChildren(); + } + + public Appearance getAppearance() { + Appearance ap = null; + Node node = getPrimitiveNode(); + if (node != null && node instanceof Shape3D) { + Shape3D shape = (Shape3D)node; + ap = shape.getAppearance(); + if (ap == null) { + ap = new Appearance(); + shape.setAppearance(ap); + } + } else if (node != null && node instanceof Primitive) { + Primitive primitive = (Primitive)node; + ap = primitive.getAppearance(); + if (ap == null) { + ap = new Appearance(); + primitive.setAppearance(ap); + } + } + return ap; + } + + public ArrayList getAppearances() { + ArrayList appearances = new ArrayList(); + appearances.add(getAppearance()); + return appearances; + } + + /** + * 衝突判定用のボリューム(ポリゴンと粗い判定用の多角柱)を取得する + * @return 衝突判定用のボリューム列 + */ + public BoundingSurface[] getBoundingSurfaces() { + if (boundingSurfaces == null) { + Node node = getPrimitiveNode(); + if (node == null) return null; + + ArrayList vertex3DList = null; + if (node instanceof Box) { + // Boxの場合 + Box box = ((Box)node); + // 頂点列を取得する + vertex3DList = getVertexList(box.getShape(Box.BACK).getGeometry()); + vertex3DList.addAll(getVertexList(box.getShape(Box.BOTTOM).getGeometry())); + vertex3DList.addAll(getVertexList(box.getShape(Box.FRONT).getGeometry())); + vertex3DList.addAll(getVertexList(box.getShape(Box.LEFT).getGeometry())); + vertex3DList.addAll(getVertexList(box.getShape(Box.RIGHT).getGeometry())); + vertex3DList.addAll(getVertexList(box.getShape(Box.TOP).getGeometry())); + } else if (node instanceof Cylinder) { + // Cylinderの場合 + Cylinder cylinder = ((Cylinder)node); + // 頂点列を取得する + vertex3DList = getVertexList(cylinder.getShape(Cylinder.BODY).getGeometry()); + vertex3DList.addAll(getVertexList(cylinder.getShape(Cylinder.BOTTOM).getGeometry())); + vertex3DList.addAll(getVertexList(cylinder.getShape(Cylinder.TOP).getGeometry())); + } else if (node instanceof Cone) { + // Coneの場合 + Cone cone = ((Cone)node); + // 頂点列を取得する + vertex3DList = getVertexList(cone.getShape(Cone.BODY).getGeometry()); + vertex3DList.addAll(getVertexList(cone.getShape(Cone.CAP).getGeometry())); + } else if (node instanceof Sphere) { + // Sphereの場合 + Sphere sphere = ((Sphere)node); + // 頂点列を取得する + vertex3DList = getVertexList(sphere.getShape(Sphere.BODY).getGeometry()); + } else if (node instanceof Shape3D) { + // Shape3Dの場合 + Shape3D shape = (Shape3D)node; + // 頂点列を取得する + vertex3DList = getVertexList(shape.getGeometry()); + } + if (vertex3DList == null) return null; + + BoundingSurface[] surfaces = new BoundingSurface[vertex3DList.size() / 3]; + + for (int i = 0; i < vertex3DList.size(); i += 3) { + Vector3d v1 = vertex3DList.get(i); + Vector3d v2 = vertex3DList.get(i + 1); + Vector3d v3 = vertex3DList.get(i + 2); + BoundingSurface bSurface = new BoundingSurface(); + bSurface.addVertex((Vector3d)v1.clone()); + bSurface.addVertex((Vector3d)v2.clone()); + bSurface.addVertex((Vector3d)v3.clone()); + bSurface.setBounds(createBoundingPolytope(v1, v2, v3)); + surfaces[i / 3] = bSurface; + } + boundingSurfaces = surfaces; + } + return boundingSurfaces; + } + + private ArrayList getVertexList(Geometry g) { + ArrayList vertex3DList = new ArrayList(); + double coordinate1[] = new double[3]; + if (g instanceof IndexedTriangleArray) { + // IndexedTriangleArray の場合 + IndexedTriangleArray triArray = (IndexedTriangleArray)g; + + // 全頂点を3D上の頂点をvertex3DListに入れていく。 + for (int i = 0; i < triArray.getIndexCount(); i++) { + triArray.getCoordinates(triArray.getCoordinateIndex(i), coordinate1); + vertex3DList.add(new Vector3d(coordinate1)); + } + } else if (g instanceof TriangleArray) { + // TriangleArray の場合 + TriangleArray triArray = (TriangleArray)g; + + // 全頂点を3D上の頂点をvertex3DListに入れていく。 + for (int i = 0; i < triArray.getVertexCount(); i++) { + triArray.getCoordinates(i, coordinate1); + vertex3DList.add(new Vector3d(coordinate1)); + } + } else if (g instanceof IndexedTriangleStripArray) { + // IndexedTriangleStripArray の場合 + IndexedTriangleStripArray triStripAttay = (IndexedTriangleStripArray)g; + int stripVertexCounts[] = new int[triStripAttay.getNumStrips()]; + triStripAttay.getStripIndexCounts(stripVertexCounts); + // 全頂点を3D上の頂点をvertex3DListに入れていく + int index = 0; + double coordinate2[] = new double[3]; + double coordinate3[] = new double[3]; + double coordinate4[] = new double[3]; + for (int i = 0; i < triStripAttay.getNumStrips(); i++) { + for (int j = 0; j < stripVertexCounts[i]; j += 2) { + triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index), coordinate1); + triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index+1), coordinate2); + triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index+2), coordinate3); + triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index+3), coordinate4); + vertex3DList.add(new Vector3d(coordinate1)); //1つめの三角形 + vertex3DList.add(new Vector3d(coordinate2)); + vertex3DList.add(new Vector3d(coordinate3)); + vertex3DList.add(new Vector3d(coordinate2)); //2つめの三角形 + vertex3DList.add(new Vector3d(coordinate4)); + vertex3DList.add(new Vector3d(coordinate3)); + index += 2; + } + } + } else if (g instanceof TriangleStripArray) { + // TriangleStripArray の場合 + TriangleStripArray triStripAttay = (TriangleStripArray)g; + int stripVertexCounts[] = new int[triStripAttay.getNumStrips()]; + triStripAttay.getStripVertexCounts(stripVertexCounts); + // 全頂点を3D上の頂点をvertex3DListに入れていく + int index = 0; + double coordinate2[] = new double[3]; + double coordinate3[] = new double[3]; + double coordinate4[] = new double[3]; + for (int i = 0; i < triStripAttay.getNumStrips(); i++) { + for (int j = 0; j < stripVertexCounts[i]; j += 2) { + triStripAttay.getCoordinates(index, coordinate1); + triStripAttay.getCoordinates(index+1, coordinate2); + triStripAttay.getCoordinates(index+2, coordinate3); + triStripAttay.getCoordinates(index+3, coordinate4); + vertex3DList.add(new Vector3d(coordinate1)); //1つめの三角形 + vertex3DList.add(new Vector3d(coordinate2)); + vertex3DList.add(new Vector3d(coordinate3)); + vertex3DList.add(new Vector3d(coordinate2)); //2つめの三角形 + vertex3DList.add(new Vector3d(coordinate4)); + vertex3DList.add(new Vector3d(coordinate3)); + index += 2; + } + } + } else if (g instanceof IndexedTriangleFanArray) { + // IndexedTriangleFanArray の場合 + IndexedTriangleFanArray triFanAttay = (IndexedTriangleFanArray)g; + int stripVertexCounts[] = new int[triFanAttay.getNumStrips()]; + triFanAttay.getStripIndexCounts(stripVertexCounts); + // 全頂点を3D上の頂点をvertex3DListに入れていく + int index = 0; + double coordinate2[] = new double[3]; + double coordinate3[] = new double[3]; + double coordinate4[] = null; + for (int i = 0; i < triFanAttay.getNumStrips(); i++) { + triFanAttay.getCoordinates(triFanAttay.getCoordinateIndex(index), coordinate1); // 中心点 + triFanAttay.getCoordinates(triFanAttay.getCoordinateIndex(index+1), coordinate2); + index += 2; + for (int j = 2; j < stripVertexCounts[i]; j++) { + triFanAttay.getCoordinates(triFanAttay.getCoordinateIndex(index), coordinate3); + vertex3DList.add(new Vector3d(coordinate1)); + vertex3DList.add(new Vector3d(coordinate2)); + vertex3DList.add(new Vector3d(coordinate3)); + coordinate4 = coordinate2; + coordinate2 = coordinate3; + coordinate3 = coordinate4; + index++; + } + } + } else if (g instanceof TriangleFanArray) { + // TriangleFanArray の場合 + TriangleFanArray triFanAttay = (TriangleFanArray)g; + int stripVertexCounts[] = new int[triFanAttay.getNumStrips()]; + triFanAttay.getStripVertexCounts(stripVertexCounts); + // 全頂点を3D上の頂点をvertex3DListに入れていく + int index = 0; + double coordinate2[] = new double[3]; + double coordinate3[] = new double[3]; + double coordinate4[] = null; + for (int i = 0; i < triFanAttay.getNumStrips(); i++) { + triFanAttay.getCoordinates(index, coordinate1); // 中心点 + triFanAttay.getCoordinates(index + 1, coordinate2); + index += 2; + for (int j = 2; j < stripVertexCounts[i]; j++) { + triFanAttay.getCoordinates(index, coordinate3); + vertex3DList.add(new Vector3d(coordinate1)); + vertex3DList.add(new Vector3d(coordinate2)); + vertex3DList.add(new Vector3d(coordinate3)); + coordinate4 = coordinate2; + coordinate2 = coordinate3; + coordinate3 = coordinate4; + index++; + } + } + } else { + // QuadArray系等は未対応 + return null; + } + return vertex3DList; + } + + protected BoundingPolytope createBoundingPolytope(Vector3d vertex1, + Vector3d vertex2, Vector3d vertex3) { + Vector3d v1 = new Vector3d(); + Vector3d v2 = new Vector3d(); + Vector3d v3 = new Vector3d(); + Vector3d v4 = new Vector3d(); + Vector3d v5 = new Vector3d(); + Vector3d v6 = new Vector3d(); + Vector3d cv1 = new Vector3d(); + Vector3d cv2 = new Vector3d(); + cv1.sub(vertex3, vertex1); + cv2.sub(vertex2, vertex1); + Vector3d cv = new Vector3d(); + cv.cross(cv1, cv2); + cv.normalize(); + cv.scale(0.01); + v1.set(vertex1); + v2.set(vertex2); + v3.set(vertex3); + v4.set(vertex1); + v4.add(cv); + v5.set(vertex2); + v5.add(cv); + v6.set(vertex3); + v6.add(cv); + + Vector3d pv1 = new Vector3d(); + Vector3d pv2 = new Vector3d(); + Vector3d pv3 = new Vector3d(); + Vector3d pn = new Vector3d(); + Vector4d[] plane = new Vector4d[5]; + + // 0 + pv1 = v1; + pv2.sub(v2, v1); + pv3.sub(v3, v1); + + pn.cross(pv2, pv3); + pn.normalize(); + plane[0] = new Vector4d(pn.x, pn.y, pn.z, -pn.dot(pv1)); + + // 1 + pv1 = v1; + pv2.sub(v4, v1); + pv3.sub(v2, v1); + + pn.cross(pv2, pv3); + pn.normalize(); + plane[1] = new Vector4d(pn.x, pn.y, pn.z, -pn.dot(pv1)); + + // 2 + pv1 = v1; + pv2.sub(v3, v1); + pv3.sub(v4, v1); + + pn.cross(pv2, pv3); + pn.normalize(); + plane[2] = new Vector4d(pn.x, pn.y, pn.z, -pn.dot(pv1)); + + // 3 + pv1 = v6; + pv2.sub(v3, v6); + pv3.sub(v5, v6); + + pn.cross(pv2, pv3); + pn.normalize(); + plane[3] = new Vector4d(pn.x, pn.y, pn.z, -pn.dot(pv1)); + + // 4 + pv1 = v6; + pv2.sub(v5, v6); + pv3.sub(v4, v6); + + pn.cross(pv2, pv3); + pn.normalize(); + plane[4] = new Vector4d(pn.x, pn.y, pn.z, -pn.dot(pv1)); + + return new BoundingPolytope(plane); + } + + public void createShadowVolume(ArrayList lights, double shadowDepth, Transform3D localToWorld) { + for (int n = 0; n < lights.size(); n++) { + shadowVolumes.add(new ShadowVolume(this, lights.get(n), shadowDepth, localToWorld)); + } + } + + public void updateShadowVolume(Transform3D localToWorld) { + for (int n = 0; n < shadowVolumes.size(); n++) { + shadowVolumes.get(n).update(localToWorld); + } + } + + public void setBumpMapping(BumpMapGenerator g) { + bumpMapGenerator = g; + bBumpMapApplied = true; + } + + public BumpMapGenerator getBumpMappingInfo() { + return bumpMapGenerator; + } + + public boolean isBumpMappingApplied() { + return bBumpMapApplied; + } + + public void setReflectionMapping(ReflectionMapGenerator g) { + reflectionMapGenerator = g; + bReflectionMapApplied = true; + } + + public ReflectionMapGenerator getReflectionMappingInfo() { + return reflectionMapGenerator; + } + + public boolean isReflectionMappingApplied() { + return bReflectionMapApplied; + } + + public boolean hasAppearancePrepared() { + if (bumpMapGenerator != null && !bumpMapGenerator.hasMapped()) return false; + if (reflectionMapGenerator != null && !reflectionMapGenerator.hasMapped()) return false; + return true; + } +} \ No newline at end of file diff --git a/src/main/java/framework/model3D/BoundingSurface.java b/src/main/java/framework/model3D/BoundingSurface.java new file mode 100644 index 0000000..cd9631a --- /dev/null +++ b/src/main/java/framework/model3D/BoundingSurface.java @@ -0,0 +1,127 @@ +package framework.model3D; + +import javax.media.j3d.BoundingPolytope; +import javax.media.j3d.BoundingSphere; +import javax.media.j3d.Bounds; +import javax.media.j3d.Transform3D; +import javax.vecmath.Matrix4d; +import javax.vecmath.Vector3d; +import javax.vecmath.Vector4d; +import java.util.ArrayList; + + +public class BoundingSurface implements Cloneable { + private Bounds bounds = null; // 粗い衝突判定用(粗い地面用) + private ArrayList children = new ArrayList(); + private ArrayList vertexList = new ArrayList(); + private static Vector4d plane[] = { new Vector4d(), new Vector4d(), new Vector4d(), + new Vector4d(), new Vector4d() }; + + public Object clone() { + BoundingSurface s = new BoundingSurface(); + if (bounds != null) { + s.setBounds((Bounds)bounds.clone()); + } + for (int i = 0; i < children.size(); i++) { + s.children.add((BoundingSurface)children.get(i).clone()); + } + for (int i = 0; i < vertexList.size(); i++) { + s.vertexList.add((Vector3d)vertexList.get(i).clone()); + } + return s; + } + + public void setBounds(Bounds bounds) { + this.bounds = bounds; + } + + public Bounds getBounds() { + return bounds; + } + + public void addVertex(Vector3d v) { + vertexList.add(v); + } + + public void addChild(BoundingSurface bs, boolean bCombineBounds) { + children.add(bs); + if (bCombineBounds) { + if (bounds == null) { + bounds = (Bounds)bs.bounds.clone(); + } else { + bounds.combine(bs.bounds); + } + } + } + + public void transform(Transform3D transform3D) { + bounds.transform(transform3D); + for (int i = 0; i < vertexList.size(); i++) { + Matrix4d mat4d = new Matrix4d(); + transform3D.get(mat4d); + double x = mat4d.m00 * vertexList.get(i).x + mat4d.m01 + * vertexList.get(i).y + mat4d.m02 * vertexList.get(i).z + + mat4d.m03; + double y = mat4d.m10 * vertexList.get(i).x + mat4d.m11 + * vertexList.get(i).y + mat4d.m12 * vertexList.get(i).z + + mat4d.m13; + double z = mat4d.m20 * vertexList.get(i).x + mat4d.m21 + * vertexList.get(i).y + mat4d.m22 * vertexList.get(i).z + + mat4d.m23; + vertexList.get(i).x = x; + vertexList.get(i).y = y; + vertexList.get(i).z = z; + } + } + + /** + * BoundingSphereとの粗い衝突判定 + * @param bs 衝突判定の対象 + * @return 衝突しているBoundingSurfaceのリスト(nullを返すことはない) + */ + public ArrayList intersect(BoundingSphere bs) { + ArrayList results = new ArrayList(); + if (children == null || children.size() == 0) { + if (bounds.intersect(bs)) { + results.add(this); + } + } else { + if (bounds == null || bounds.intersect(bs)) { + for (int n = 0; n < children.size(); n++) { + results.addAll(children.get(n).intersect(bs)); + } + } + } + return results; + } + + /** + * OBBとの詳細な衝突判定 + * @param obb 衝突判定の対象 + * @return 衝突判定の結果 + */ + public CollisionResult intersect(OBB obb) { + if (children == null || children.size() == 0) { + // 葉の場合は、凸ポリゴンを立ち上げた薄い多角柱 + if (bounds instanceof BoundingPolytope) { + ((BoundingPolytope)bounds).getPlanes(plane); + CollisionResult cr = obb.intersect(plane[0]); // obbが底面を含む無限平面と交わっているか? + if (cr != null) { + // 無限平面と交わっている場合、無限平面との衝突点が凸ポリゴンの内部に位置するか? + if (GeometryUtility.inside(vertexList, cr.collisionPoint.getVector3d(), cr.normal)) { + return cr; + } + } + } + return null; + } else { + for (int n = 0; n < children.size(); n++) { + CollisionResult cr = children.get(n).intersect(obb); + if (cr != null) { + return cr; + } + } + return null; + } + } +} diff --git a/src/main/java/framework/model3D/BumpMapGenerator.java b/src/main/java/framework/model3D/BumpMapGenerator.java new file mode 100644 index 0000000..f39dc2c --- /dev/null +++ b/src/main/java/framework/model3D/BumpMapGenerator.java @@ -0,0 +1,28 @@ +package framework.model3D; + +import javax.media.j3d.TexCoordGeneration; +import javax.media.j3d.Texture; + +public class BumpMapGenerator extends MapGenerator { + private Texture bumpMap = null; + private TexCoordGeneration texCoorgGen = null; + private boolean bHorizontal = false; + + public BumpMapGenerator(Texture bumpMap, TexCoordGeneration texCoorgGen, boolean bHorizontal) { + this.bumpMap = bumpMap; + this.texCoorgGen = texCoorgGen; + this.bHorizontal = bHorizontal; + } + + public Texture getBumpMap() { + return bumpMap; + } + + public TexCoordGeneration getTexCoordGeneration() { + return texCoorgGen; + } + + public boolean isHorizontal() { + return bHorizontal; + } +} diff --git a/src/main/java/framework/model3D/CollisionResult.java b/src/main/java/framework/model3D/CollisionResult.java new file mode 100644 index 0000000..1e5319b --- /dev/null +++ b/src/main/java/framework/model3D/CollisionResult.java @@ -0,0 +1,12 @@ +package framework.model3D; + +import javax.vecmath.Vector3d; +import java.util.ArrayList; + + +public class CollisionResult { + public Position3D collisionPoint = new Position3D(); + public ArrayList collisionPoints; + public Vector3d normal = new Vector3d(); + public double length = 0.0; +} diff --git a/src/main/java/framework/model3D/ContainerModel.java b/src/main/java/framework/model3D/ContainerModel.java new file mode 100644 index 0000000..8644d75 --- /dev/null +++ b/src/main/java/framework/model3D/ContainerModel.java @@ -0,0 +1,42 @@ +package framework.model3D; + +import javax.media.j3d.Transform3D; + +/** + * 子供を持つモデル + * @author 新田直也 + * + */ +public class ContainerModel extends Model3D { + private Model3D[] children; + private Transform3D defaultTransform = null; + + public ContainerModel(String name,Model3D[] children) { + this.children = children; + this.name = name; + this.defaultTransform = null; + } + + public ContainerModel(String name, Model3D[] children, + Transform3D transform) { + this.children = children; + this.name = name; + this.defaultTransform = transform; + } + + public Object3D createObject() { + Object3D[] objChild = new Object3D[children.length]; + for(int i = 0;i < children.length;i++){ + objChild[i] = (Object3D) children[i].createObject(); + } + return new Object3D(name, objChild, defaultTransform); + } + + public Object3D createObjectSharingAppearance() { + Object3D[] objChild = new Object3D[children.length]; + for(int i = 0;i < children.length;i++){ + objChild[i] = (Object3D) children[i].createObjectSharingAppearance(); + } + return new Object3D(name, objChild, defaultTransform); + } +} diff --git a/src/main/java/framework/model3D/FresnelsReflectionMapGenerator.java b/src/main/java/framework/model3D/FresnelsReflectionMapGenerator.java new file mode 100644 index 0000000..7546579 --- /dev/null +++ b/src/main/java/framework/model3D/FresnelsReflectionMapGenerator.java @@ -0,0 +1,16 @@ +package framework.model3D; + +import javax.vecmath.Color4f; + +public class FresnelsReflectionMapGenerator extends ReflectionMapGenerator { + private boolean bTransparent = true; + + public FresnelsReflectionMapGenerator(Color4f blendColor, boolean bTransparent) { + super(blendColor); + this.bTransparent = bTransparent; + } + + public boolean isTransparent() { + return bTransparent; + } +} diff --git a/src/main/java/framework/model3D/GeometryCollector.java b/src/main/java/framework/model3D/GeometryCollector.java new file mode 100644 index 0000000..0ae81dc --- /dev/null +++ b/src/main/java/framework/model3D/GeometryCollector.java @@ -0,0 +1,124 @@ +package framework.model3D; + +import javax.media.j3d.Geometry; +import javax.media.j3d.IndexedGeometryArray; +import javax.media.j3d.IndexedTriangleArray; +import javax.media.j3d.Shape3D; +import java.util.ArrayList; + +public class GeometryCollector extends ObjectVisitor{ + private int totalVertexCount = 0; + private int totalIndexCount = 0; + private ArrayList geometryList = new ArrayList(); // 葉のとき、Geometryを保存 + IndexedGeometryArray geometry = null; +// private Stack transforms = new Stack(); + + int i=0; + int j=0; + + //コンストラクタ +// public GeometryCollector() { +// Transform3D t = new Transform3D(); +// transforms.push(t); +// } + + @Override + public void preVisit(Object3D obj) { + // TODO Auto-generated method stub + if(obj.hasChildren()){ +// System.out.println("pre"+i); +// i++; + }else{ +// System.out.println("pre"+i+" Leaf"); +// i++; + Geometry g = ((Shape3D)obj.getBody().getPrimitiveNode()).getGeometry(); + geometryList.add(g); + totalVertexCount += ((IndexedGeometryArray)g).getVertexCount(); + totalIndexCount += ((IndexedGeometryArray)g).getIndexCount(); + } + + + } + + @Override + public void postVisit(Object3D obj) { + // TODO Auto-generated method stub + if(obj.hasChildren()){ +// System.out.println("post"+j); + j++; + }else{ +// System.out.println("post"+j+" Leaf"); + j++; + } + } + + public Geometry getGeometry(){ + if (geometry == null) { + // リスト中の複数の Geometry を1つにまとめる + int coordinateOfs = 0; + int indexOfs = 0; + double coodinates[] = new double[totalVertexCount * 3]; + int indicies[] = new int[totalIndexCount]; + for (int n = 0; n < geometryList.size(); n++) { + IndexedGeometryArray geometry = (IndexedGeometryArray)geometryList.get(n); + double tmpCoordinates[] = new double[geometry.getVertexCount() * 3]; + geometry.getCoordinates(0, tmpCoordinates); + System.arraycopy(tmpCoordinates, 0, coodinates, coordinateOfs * 3, geometry.getVertexCount() * 3); + int tmpIndicies[] = new int[geometry.getIndexCount()]; + geometry.getCoordinateIndices(0, tmpIndicies); + for (int m = 0; m < geometry.getIndexCount(); m++) { + tmpIndicies[m] += coordinateOfs; + } + System.arraycopy(tmpIndicies, 0, indicies, indexOfs, geometry.getIndexCount()); + coordinateOfs += geometry.getVertexCount(); + indexOfs += geometry.getIndexCount(); + } + geometry = new IndexedTriangleArray(totalVertexCount, IndexedTriangleArray.COORDINATES, totalIndexCount); + geometry.setCoordinates(0, coodinates); + geometry.setCoordinateIndices(0, indicies); + + // 重複している頂点を1つにまとめる + GeometryUtility.compressGeometry(geometry); + +// // IndexedTriangleStripArray に変換(重複している頂点を1つにまとめるため) +// GeometryInfo gi = new GeometryInfo(geometry); +// Stripifier sf = new Stripifier(); +// sf.stripify(gi); +// geometry = gi.getIndexedGeometryArray(); // IndexedTriangleStripArray が返される +// +// // IndexedTriangleArray に変換(GeometryGraph が IndexedTriangleStripArray に未対応のため) +// if (geometry instanceof IndexedTriangleStripArray) { +// int numStrips = ((IndexedTriangleStripArray)geometry).getNumStrips(); +// int indexCounts[] = new int[numStrips]; +// ((IndexedTriangleStripArray)geometry).getStripIndexCounts(indexCounts); +// int dstIndicies[] = new int[(((IndexedTriangleStripArray)geometry).getIndexCount() - numStrips * 2) * 3]; +// int dstOfs = 0; +// int srcOfs = 0; +// for (int s = 0; s < numStrips; s++) { +// for (int i = 2; i < indexCounts[s]; i += 2) { +// dstIndicies[dstOfs] = ((IndexedTriangleStripArray)geometry).getCoordinateIndex(srcOfs + i - 2); +// dstIndicies[dstOfs + 1] = ((IndexedTriangleStripArray)geometry).getCoordinateIndex(srcOfs + i - 1); +// dstIndicies[dstOfs + 2] = ((IndexedTriangleStripArray)geometry).getCoordinateIndex(srcOfs + i); +// dstOfs += 3; +// if (i + 1 < indexCounts[s]) { +// dstIndicies[dstOfs] = ((IndexedTriangleStripArray)geometry).getCoordinateIndex(srcOfs + i); +// dstIndicies[dstOfs + 1] = ((IndexedTriangleStripArray)geometry).getCoordinateIndex(srcOfs + i - 1); +// dstIndicies[dstOfs + 2] = ((IndexedTriangleStripArray)geometry).getCoordinateIndex(srcOfs + i + 1); +// dstOfs += 3; +// } +// } +// srcOfs += indexCounts[s]; +// } +// +// int vertexCount = ((IndexedTriangleStripArray)geometry).getVertexCount(); +// coodinates = new double[vertexCount * 3]; +// ((IndexedTriangleStripArray)geometry).getCoordinates(0, coodinates); +// +// geometry = new IndexedTriangleArray(vertexCount, IndexedTriangleArray.COORDINATES, dstIndicies.length); +// geometry.setCoordinates(0, coodinates); +// geometry.setCoordinateIndices(0, dstIndicies); +// } + } + return geometry; + } +} diff --git a/src/main/java/framework/model3D/GeometryUtility.java b/src/main/java/framework/model3D/GeometryUtility.java new file mode 100644 index 0000000..67792a1 --- /dev/null +++ b/src/main/java/framework/model3D/GeometryUtility.java @@ -0,0 +1,287 @@ +package framework.model3D; + +import javax.media.j3d.IndexedGeometryArray; +import javax.vecmath.*; +import java.util.ArrayList; +import java.util.Hashtable; + +public class GeometryUtility { + public static final double TOLERANCE = 0.0000002; + public static final Vector3d X_AXIS = new Vector3d(1.0, 0.0, 0.0); + public static final Vector3d Y_AXIS = new Vector3d(0.0, 1.0, 0.0); + public static final Vector3d Z_AXIS = new Vector3d(0.0, 0.0, 1.0); + + public static ProjectionResult projection3D( + ArrayList vertex3Dlist, Vector3d axis) { + double k = 0.0; + int i = 0; + + ProjectionResult pr = new ProjectionResult(); + + axis.normalize(); + + for (i = 0; i < vertex3Dlist.size(); i++) { + Vector3d p = vertex3Dlist.get(i); + Vector3d p1 = new Vector3d(); + + k = p.dot(axis); + p1.scaleAdd(-k, axis, p); + + if (i == 0 || k >= pr.max) { + pr.max = k; + } else if (i == 0 || k <= pr.min) { + pr.min = k; + } + pr.vertexList.add(p1); + } + return pr; + } + + public static ProjectionResult projection2D( + ArrayList vertex2Dlist, Vector3d axis) { + int i = 0; + double k = 0.0; + // System.out.println("point3:"+axis); + if (axis.x != 0 || axis.y != 0 || axis.z != 0) { + axis.normalize(); + } + // System.out.println("point3_1:"+axis); + ProjectionResult pr = new ProjectionResult(); + + for (i = 0; i < vertex2Dlist.size(); i++) { + Vector3d p = vertex2Dlist.get(i); + + k = p.dot(axis); + // System.out.println("k:"+k); + // System.out.println("point3:"+axis); + if (i == 0 || k >= pr.max) { + pr.max = k; + } else if (i == 0 || k <= pr.min) { + pr.min = k; + } + } + return pr; + } + + // 以下、頂点の表裏の判定メソッド + public static boolean inside(Vector3d v, Vector4d plane) { + // System.out.println("vertex:" + v.x + "," + v.y + "," + v.z); + // System.out.println("plane:" + plane.x + "," + plane.y + "," + plane.z + // + "," + plane.w); + Vector3d pv = new Vector3d(plane.x, plane.y, plane.z); + // Vector3d nv = (Vector3d)pv.clone(); + // nv.scaleAdd(plane.w / pv.lengthSquared(),v); + if (pv.dot(v) + plane.w <= TOLERANCE) { + // System.out.println("内部判定!!"+nv.dot(pv)); + pv = null; + return true; + } + // System.out.println("外部判定!!"+nv.dot(pv)); + pv = null; + return false; + } + + /** + * 3点を通る平面を作成する(ただし、v1, v2, v3の内容が書き換えられるので注意) + * + * @param v1 + * --- 1点目の座標 + * @param v2 + * --- 2点目の座標 + * @param v3 + * --- 3点目の座標 + * @return v1, v2, v3を通る平面 + */ + public static Vector4d createPlane(Vector3d v1, Vector3d v2, Vector3d v3) { + v2 = (Vector3d)v2.clone(); + v3 = (Vector3d)v3.clone(); + v2.sub(v1); + v3.sub(v1); + v2.cross(v2, v3); + if (v2.length() < TOLERANCE) return null; + v2.normalize(); + return new Vector4d(v2.x, v2.y, v2.z, -v2.dot(v1)); + } + + /** + * + * 平面と直線の交点を求める + * + * @param plane + * 平面 + * @param v1 + * 直線上の点1 + * @param v2 + * 直線状の点2 + * @return 交点 + */ + public static Vector3d intersect(Vector4d plane, Vector3d v1, Vector3d v2) { + Vector3d n = new Vector3d(plane.x, plane.y, plane.z); + Vector3d v21 = (Vector3d) v2.clone(); + v21.sub(v1); + double s = n.dot(v21); + if (Math.abs(s) < TOLERANCE) return null; + v21.scale((-plane.w - n.dot(v1)) / s); + v21.add(v1); + return v21; + } + + /** + * + * 与えられた点に最も近い直線上の点を求める + * + * @param src 直線(線分)の始点 + * @param dst 直線(線分)の終点 + * @param pos 対象の点 + * @return srcとdstを通る直線上でposに最も近い点 + */ + public static Vector3d nearest(Vector3d src, Vector3d dst, Vector3d pos) { + Vector3d dir = (Vector3d)dst.clone(); + dir.sub(src); + dir.normalize(); + Vector3d p = (Vector3d)pos.clone(); + p.sub(src); + dir.scale(p.dot(dir)); + dir.add(src); + return dir; + } + + /** + * + * 指定された点が凸ポリゴンの内部に包含されているか? + * + * @param vertexList + * 凸ポリゴンの頂点列 + * @param point + * 指定点 + * @return true --- 包含されている, false --- 包含されていない + */ + public static boolean inside(ArrayList vertexList, Vector3d point, Vector3d normal) { + boolean inside = true; + for (int i = 0; i < vertexList.size(); i++) { + // ポリゴンの各辺に対して衝突点が右側か左側か? + Vector3d center = (Vector3d) point.clone(); + Vector3d v2 = (Vector3d) (vertexList.get((i + 1) + % vertexList.size()).clone()); + Vector3d v1 = (Vector3d) vertexList.get(i).clone(); + center.sub(v1); + v2.sub(v1); + v1.cross(v2, center); + if (normal.dot(v1) < -GeometryUtility.TOLERANCE) { + inside = false; + break; + } + } + // すべて右側、またはすべて左側だった場合、凸ポリゴンの内部に位置したと考える + return inside; + } + + // IndexedGeometryArrayのインデックスのうち、同じ座標をさすインデックスを置き換える + public static void compressGeometry(IndexedGeometryArray g) { + // ①Hashtableを作りながら、representation[]を作る + Hashtable> h = new Hashtable>(); + Point3d p = new Point3d(); + Point3d p2 = new Point3d(); + double hash; + ArrayList list; + int[] representation = new int[g.getVertexCount()]; + + for (int i = 0; i < g.getVertexCount(); i++) { + g.getCoordinate(i, p); + + hash = p.getX() + p.getY() + p.getZ(); + + list = h.get(new Double(hash)); + + if (list == null) {// hashに対応する要素がない場合 + // Hashtableを作る + list = new ArrayList(); + list.add(new Integer(i)); + h.put(new Double(hash), list); + // representation[]を作る + representation[i] = i; + } else { + boolean bFound = false; + for (int j = 0; j < list.size(); j++) { + g.getCoordinate(list.get(j).intValue(), p2); + if (p.getX() == p2.getX() && p.getY() == p2.getY() + && p.getZ() == p2.getZ()) { + representation[i] = list.get(j).intValue(); + bFound = true; + break; + } + } + if (!bFound) { + list.add(new Integer(i)); + // representation[]を作る + representation[i] = i; + } + } + } + + // ②indexの置き換え + for (int i = 0; i < g.getIndexCount(); i++) { + int index = representation[g.getCoordinateIndex(i)]; + g.setCoordinateIndex(i, index); + + } + + } + + public static Matrix3d calcRotationForView(Vector3d viewLine) { + Vector3d v1 = (Vector3d)viewLine.clone(); + v1.normalize(); + Vector3d v2 = new Vector3d(); + v2.cross(v1, Y_AXIS); + double angle2 = Math.atan2(-v2.z, v2.x); + double angle1 = Math.PI / 2.0 - Math.acos(v1.dot(Y_AXIS)); + Matrix3d rot1 = new Matrix3d(); + rot1.rotX(angle1); + Matrix3d rot2 = new Matrix3d(); + rot2.rotY(angle2); + rot2.mul(rot1); + return rot2; + } + + public static Quaternion3D calcQuaternionForView(Vector3d viewLine) { + Vector3d v1 = (Vector3d)viewLine.clone(); + v1.normalize(); + Vector3d v2 = new Vector3d(); + v2.cross(v1, Y_AXIS); + double angle2 = Math.atan2(-v2.z, v2.x); + double angle1 = Math.PI / 2.0 - Math.acos(v1.dot(Y_AXIS)); + Quaternion3D quat1 = new Quaternion3D(new AxisAngle4d(X_AXIS, angle1)); + Quaternion3D quat2 = new Quaternion3D(new AxisAngle4d(Y_AXIS, angle2)); + + return quat1.mul(quat2); + } + + public static Quaternion3D calcQuaternionFromSrcToDst(Vector3d src, Vector3d dst) { + Vector3d v1 = new Vector3d(); + Vector3d v2 = new Vector3d(); + v1.cross(src, Y_AXIS); + v2.cross(dst, Y_AXIS); + Quaternion3D quat1; + Quaternion3D quat2; + double angle2 = Math.acos(Y_AXIS.dot(src) / src.length()) - Math.acos(Y_AXIS.dot(dst) / dst.length()); + if (Math.abs(v2.length()) < TOLERANCE) { + v1.normalize(); + quat1 = new Quaternion3D(); + quat2 = new Quaternion3D(new AxisAngle4d(v1, angle2)); + } else { + if (Math.abs(v1.length()) < TOLERANCE) { + quat1 = new Quaternion3D(); + } else { + v1.normalize(); + v2.normalize(); + double cos1 = v1.dot(v2); + v1.cross(v1, v2); + double sin1 = v1.dot(Y_AXIS); + double angle1 = Math.atan2(sin1, cos1); + quat1 = new Quaternion3D(new AxisAngle4d(Y_AXIS, angle1)); + } + quat2 = new Quaternion3D(new AxisAngle4d(v2, angle2)); + } + return quat1.mul(quat2); + } +} \ No newline at end of file diff --git a/src/main/java/framework/model3D/IViewer3D.java b/src/main/java/framework/model3D/IViewer3D.java new file mode 100644 index 0000000..0ae4f47 --- /dev/null +++ b/src/main/java/framework/model3D/IViewer3D.java @@ -0,0 +1,14 @@ +package framework.model3D; + +import javax.media.j3d.GraphicsContext3D; +import javax.media.j3d.Light; +import javax.media.j3d.Transform3D; +import java.util.ArrayList; + + +public interface IViewer3D { + public abstract void update(ArrayList lights, BackgroundBox skyBox); + public abstract void setGraphicsContext3D(GraphicsContext3D graphicsContext3D); + public abstract void setModelTransform(Transform3D t); + public abstract void draw(BaseObject3D obj); +} diff --git a/src/main/java/framework/model3D/LeafModel.java b/src/main/java/framework/model3D/LeafModel.java new file mode 100644 index 0000000..3ad5d1e --- /dev/null +++ b/src/main/java/framework/model3D/LeafModel.java @@ -0,0 +1,31 @@ +package framework.model3D; +import javax.media.j3d.Appearance; +import javax.media.j3d.Geometry; + + +public class LeafModel extends Model3D{ + + private Geometry g; + private Appearance a; + + public LeafModel(String name, Geometry g, Appearance a){ + this.name = name; + this.g = g; + this.a = a; + } + + public Object3D createObject(){ + Appearance a2 = (Appearance)a.cloneNodeComponent(true); + a2.setCapability(Appearance.ALLOW_TEXTURE_READ); + a2.setCapability(Appearance.ALLOW_TEXTURE_WRITE); + a2.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_READ); + a2.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_WRITE); + Object3D obj = new Object3D(name,g,a2); + return obj; + } + + public Object3D createObjectSharingAppearance(){ + Object3D obj = new Object3D(name,g,a); + return obj; + } +} diff --git a/src/main/java/framework/model3D/MapGenerator.java b/src/main/java/framework/model3D/MapGenerator.java new file mode 100644 index 0000000..aff22a3 --- /dev/null +++ b/src/main/java/framework/model3D/MapGenerator.java @@ -0,0 +1,17 @@ +package framework.model3D; + +public abstract class MapGenerator { + private boolean bMapped = false; + + public MapGenerator() { + super(); + } + + public void setMapped() { + bMapped = true; + } + + public boolean hasMapped() { + return bMapped; + } +} \ No newline at end of file diff --git a/src/main/java/framework/model3D/Model3D.java b/src/main/java/framework/model3D/Model3D.java new file mode 100644 index 0000000..fa9b824 --- /dev/null +++ b/src/main/java/framework/model3D/Model3D.java @@ -0,0 +1,9 @@ +package framework.model3D; + + + +public abstract class Model3D { + String name; + public abstract Object3D createObject(); + public abstract Object3D createObjectSharingAppearance(); +} diff --git a/src/main/java/framework/model3D/ModelFactory.java b/src/main/java/framework/model3D/ModelFactory.java new file mode 100644 index 0000000..be7f6ef --- /dev/null +++ b/src/main/java/framework/model3D/ModelFactory.java @@ -0,0 +1,872 @@ +package framework.model3D; + +import com.sun.j3d.utils.geometry.GeometryInfo; +import com.sun.j3d.utils.geometry.NormalGenerator; +import com.sun.j3d.utils.image.TextureLoader; +import com.sun.j3d.utils.universe.SimpleUniverse; +import framework.schedule.ScheduleManager; +import framework.schedule.TaskController; + +import javax.media.j3d.*; +import javax.vecmath.Vector2f; +import javax.vecmath.Vector3f; +import java.io.*; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * 3次元モデルを生成するためのクラス + * @author 新田直也 + * + */ +public class ModelFactory { + public static final String STL_FILE_EXTENSION = ".stl"; + public static final String OBJ_FILE_EXTENSION = ".obj"; + + /** + * 3次元モデルを階層構造と各部の名称とともにファイルから読み込む + * @param fileName ファイル名 + * @return + */ + public static Model3D loadModel(String fileName){ + return loadModel(fileName, false, false); + } + + /** + * 3次元モデルを階層構造と各部の名称とともにファイルから読み込む(透明テクスチャの有無の指定付き) + * @param fileName ファイル名 + * @param bTransparent 透明テクスチャがあるか否か + * @param bCalcNormal TODO + * @return + */ + public static Model3D loadModel(String fileName, boolean bTransparent, boolean bCalcNormal){ +// System.out.println(fileName); + if (fileName.toLowerCase().endsWith(STL_FILE_EXTENSION)) { + // STLファイル読み込み + try { + return loadStlFile(fileName, null, bCalcNormal); + } catch (IOException | ModelFileFormatException e) { + e.printStackTrace(); + } + } else if (fileName.toLowerCase().endsWith(OBJ_FILE_EXTENSION)) { + // OBJファイル読み込み + try { + return loadObjFile(fileName, bCalcNormal); + } catch (IOException | ModelFileFormatException e) { + e.printStackTrace(); + } + } + + return null; + } + + /** + * STLファイル(アスキー、バイナリ)の読み込み + * @param fileName ファイル名 + * @param ap 3Dモデルに適用する表面属性 + * @param bCalcNormal 法線の再計算を行うか + * @return 読み込んだ3Dモデル + * @throws IOException + * @throws ModelFileFormatException + */ + public static Model3D loadStlFile(String fileName, Appearance ap, boolean bCalcNormal) throws IOException, ModelFileFormatException { + // STLファイル読み込み + FileInputStream in = new FileInputStream(fileName); + BufferedInputStream bis = new BufferedInputStream(in); + byte[] headerBin = new byte[80]; + int n = bis.read(headerBin); + if (headerBin[0] == 's' && + headerBin[1] == 'o' && + headerBin[2] == 'l' && + headerBin[3] == 'i' && + headerBin[4] == 'd' && + headerBin[5] == ' ') { + bis.close(); + + // STLはテキストファイル + File file = new File(fileName); + FileReader filereader = new FileReader(file); + BufferedReader br = new BufferedReader(filereader); + Model3D model = loadStlAsciiFile(br, ap, bCalcNormal); + br.close(); + return model; + } else if (n == 80) { + // STLはバイナリファイル + Model3D model = loadStlBinaryFile(bis, headerBin, ap, bCalcNormal); + bis.close(); + return model; + } + return null; + } + + private static Model3D loadStlAsciiFile(BufferedReader br, Appearance ap, boolean bCalcNormal) throws IOException, ModelFileFormatException { + // ヘッダ読み込み + String header = br.readLine(); + if (header == null) { + br.close(); + throw new ModelFileFormatException(); + } + header.trim(); + String headerContents[] = header.split(" "); + String name; + if (headerContents.length > 0 && headerContents[0].equals("solid")) { + if (headerContents.length > 1) name = headerContents[1]; + else name = ""; + } else { + System.out.println("STL file's header error!!"); + br.close(); + throw new ModelFileFormatException(); + } + + // Facet読み込み + ArrayList verticies = new ArrayList(); + String line; + while ((line = br.readLine()) != null && !line.startsWith("endsolid ")) { + // facet normal ... + line.trim(); + String[] data = line.split(" "); + if (data.length < 1 || !data[0].equals("facet")) { + System.out.println("Expected facet normal ..."); + br.close(); + throw new ModelFileFormatException(); + } + + // outer loop + line = br.readLine(); + if (line == null) { + System.out.println("Expected outer loop"); + br.close(); + throw new ModelFileFormatException(); + } + line.trim(); + if (!line.equals("outer loop")) { + System.out.println("Expected outer loop"); + br.close(); + throw new ModelFileFormatException(); + } + + // vertex * 3 + for (int i = 0; i < 3; i++) { + line = br.readLine(); + if (line == null) { + System.out.println("Expected vertex x y z"); + br.close(); + throw new ModelFileFormatException(); + } + line.trim(); + data = line.split(" "); + if (data.length < 4 || !data[0].equals("vertex")) { + System.out.println("Expected vertex x y z"); + br.close(); + throw new ModelFileFormatException(); + } + double[] vertex = new double[]{ + Double.parseDouble(data[1]), // X座標 + Double.parseDouble(data[2]), // Y座標 + Double.parseDouble(data[3]) // Z座標 + }; + verticies.add(vertex); // Z座標 + } + + // endloop + line = br.readLine(); + if (line == null) { + System.out.println("Expected endloop"); + br.close(); + throw new ModelFileFormatException(); + } + line.trim(); + if (!line.equals("endloop")) { + System.out.println("Expected endloop"); + br.close(); + throw new ModelFileFormatException(); + } + + // endfacet + line = br.readLine(); + if (line == null) { + System.out.println("Expected endfacet"); + br.close(); + throw new ModelFileFormatException(); + } + line.trim(); + if (!line.equals("endfacet")) { + System.out.println("Expected endfacet"); + br.close(); + throw new ModelFileFormatException(); + } + } + + TriangleArray triArray = new TriangleArray(verticies.size(), TriangleArray.COORDINATES); + for (int n = 0; n < verticies.size(); n++) { + triArray.setCoordinate(n, verticies.get(n)); + } + if (ap == null) ap = new Appearance(); + return new LeafModel(name, triArray, ap); + } + + private static Model3D loadStlBinaryFile(BufferedInputStream bis, byte[] header, Appearance ap, boolean bCalcNormal) throws IOException, ModelFileFormatException { + // Facet数読み込み + Integer numTri = readInt(bis); + if (numTri == null) { + System.out.println("Expected vertex count"); + bis.close(); + throw new ModelFileFormatException(); + } + + // Facet読み込み + ArrayList verticies = new ArrayList(); + ArrayList normals = new ArrayList(); + Vector3f[] vertex = new Vector3f[] { + new Vector3f(), + new Vector3f(), + new Vector3f() + }; + + for (int n = 0; n < numTri; n++) { + Float nx = readFloat(bis); // 法線X成分 + if (nx == null) { + System.out.println("Expected normal"); + bis.close(); + throw new ModelFileFormatException(); + } + + Float ny = readFloat(bis); // 法線Y成分 + if (ny == null) { + System.out.println("Expected normal"); + bis.close(); + throw new ModelFileFormatException(); + } + + Float nz = readFloat(bis); // 法線Z成分 + if (nz == null) { + System.out.println("Expected normal"); + bis.close(); + throw new ModelFileFormatException(); + } + + for (int i = 0; i < 3; i++) { + Float x = readFloat(bis); // X座標 + if (x == null) { + System.out.println("Expected vertex" + (i + 1)); + bis.close(); + throw new ModelFileFormatException(); + } + + Float y = readFloat(bis); // Y座標 + if (y == null) { + System.out.println("Expected vertex" + (i + 1)); + bis.close(); + throw new ModelFileFormatException(); + } + + Float z = readFloat(bis); // Z座標 + if (z == null) { + System.out.println("Expected vertex" + (i + 1)); + bis.close(); + throw new ModelFileFormatException(); + } + + verticies.add(new float[]{x, y, z}); + vertex[i].x = x; + vertex[i].y = y; + vertex[i].z = z; + } + if (bCalcNormal) { + vertex[1].sub(vertex[0]); + vertex[2].sub(vertex[0]); + vertex[1].normalize(); + vertex[2].normalize(); + vertex[0].cross(vertex[1], vertex[2]); + nx = vertex[0].x; + ny = vertex[0].y; + nz = vertex[0].z; + } + normals.add(new float[]{nx, ny, nz}); + normals.add(new float[]{nx, ny, nz}); + normals.add(new float[]{nx, ny, nz}); + + byte[] optionBin = new byte[2]; + if (bis.read(optionBin) < 2) { + System.out.println("Expected option"); + bis.close(); + throw new ModelFileFormatException(); + } + } + + TriangleArray triArray = new TriangleArray(verticies.size(), TriangleArray.COORDINATES | TriangleArray.NORMALS); + for (int n = 0; n < verticies.size(); n++) { + triArray.setCoordinate(n, verticies.get(n)); + triArray.setNormal(n, normals.get(n)); + } + + if (ap == null) ap = new Appearance(); + return new LeafModel(new String(header), triArray, ap); + } + + /** + * OBJファイルの読み込み + * @param fileName ファイル名 + * @param bCalcNormal 法線の再計算を行うか + * @return 読み込んだ3Dモデル + * @throws IOException + * @throws ModelFileFormatException + */ + public static Model3D loadObjFile(String fileName, boolean bCalcNormal) throws IOException, ModelFileFormatException { + File file = new File(fileName); + FileReader filereader = new FileReader(file); + BufferedReader br = new BufferedReader(filereader); + + HashMap appearances = new HashMap(); + ArrayList objects = new ArrayList(); + ArrayList groups = new ArrayList(); + ArrayList subGroups = new ArrayList(); + ArrayList objCoordinates = new ArrayList(); + ArrayList objTexCoordinates = new ArrayList(); + ArrayList objNormals = new ArrayList(); + ArrayList> subGroupCoordinateIndicies = new ArrayList>(); + ArrayList> subGroupTexCoordIndicies = new ArrayList>(); + ArrayList> subGroupNormalIndicies = new ArrayList>(); + String objectName = ""; + String groupName = ""; + String subGroupName = ""; + Appearance subGroupAp = null; + + String line = null; + while ((line = br.readLine()) != null) { + line.trim(); + String data[] = line.split(" "); + if (data[0].equals("#")) { + // コメント + } else if (data[0].equals("mtllib")) { + // 材質ファイルの指定 + String dir = new File(fileName).getParent(); + String mtlFileName = line.substring(line.indexOf(" ") + 1); + String mtlFilePath = new File(dir, mtlFileName).getPath(); + appearances = loadMtlFile(mtlFilePath); + } else if (data[0].equals("o")) { + // オブジェクトの開始 + if (groups.size() > 0 || subGroups.size() > 0 || subGroupCoordinateIndicies.size() > 0) { + // 前のオブジェクトのグループの残り、サブグループの残り、ストリップの残りを新しいオブジェクトとして追加 + Model3D newObject = createRemainingObject(objectName, groups, groupName, objCoordinates, + objTexCoordinates, objNormals, subGroups, subGroupName, subGroupAp, + subGroupCoordinateIndicies, subGroupTexCoordIndicies, subGroupNormalIndicies); + objects.add(newObject); + groups.clear(); + } + + if (data.length < 1) { + System.out.println("Expected object's name"); + throw new ModelFileFormatException(); + } + objectName = data[1]; + } else if (data[0].equals("g")) { + // グループの開始 + if (subGroups.size() > 0 || subGroupCoordinateIndicies.size() > 0) { + // 前のグループのサブグループの残り、ストリップの残りを新しいグループとして追加 + Model3D newGroup = createRemainingGroup(groupName, objCoordinates, objTexCoordinates, objNormals, subGroups, + subGroupName, subGroupAp, subGroupCoordinateIndicies, subGroupTexCoordIndicies, subGroupNormalIndicies); + groups.add(newGroup); + subGroups.clear(); + } + + if (data.length < 1) { + System.out.println("Expected group's name"); + throw new ModelFileFormatException(); + } + groupName = data[1]; + } else if (data[0].equals("usemtl")) { + // 材質の使用(サブグループの開始) + if (subGroupCoordinateIndicies.size() > 0) { + // 前のサブグループのストリップの残りを新しいサブグループとして追加 + Model3D newSubGroup = createRemainingSubGroup(objCoordinates, objTexCoordinates, objNormals, + subGroupName, subGroupAp, subGroupCoordinateIndicies, subGroupTexCoordIndicies, subGroupNormalIndicies); + subGroups.add(newSubGroup); + subGroupCoordinateIndicies.clear(); + subGroupTexCoordIndicies.clear(); + subGroupNormalIndicies.clear(); + } + + if (data.length < 1) { + System.out.println("Expected mtl file's name"); + throw new ModelFileFormatException(); + } + subGroupAp = appearances.get(data[1]); + subGroupName = groupName + "_" + data[1] + subGroups.size(); // ※サブグループの名前が重複しないようにする処理 + } else if (data[0].equals("v")) { + // 頂点座標データ + if (data.length < 4) { + System.out.println("Expected vertex coordinate"); + throw new ModelFileFormatException(); + } + float x = Float.parseFloat(data[1]); + float y = Float.parseFloat(data[2]); + float z = Float.parseFloat(data[3]); + Vector3f v = new Vector3f(x, y, z); + objCoordinates.add(v); + } else if (data[0].equals("vt")) { + // テクスチャ座標データ + if (data.length < 3) { + System.out.println("Expected texture coordinate"); + throw new ModelFileFormatException(); + } + float x = Float.parseFloat(data[1]); + float y = Float.parseFloat(data[2]); + Vector2f v = new Vector2f(x, y); + objTexCoordinates.add(v); + } else if (data[0].equals("vn")) { + // 法線ベクトルデータ + if (data.length < 4) { + System.out.println("Expected normal vector"); + throw new ModelFileFormatException(); + } + float x = Float.parseFloat(data[1]); + float y = Float.parseFloat(data[2]); + float z = Float.parseFloat(data[3]); + Vector3f v = new Vector3f(x, y, z); + objNormals.add(v); + } else if (data[0].equals("f")) { + // ポリゴン(ストリップ)データ + if (data.length < 4) { + System.out.println("At least 3 verticies are needed"); + throw new ModelFileFormatException(); + } + ArrayList coordinateIndicies = new ArrayList(); + ArrayList texCoordIndicies = new ArrayList(); + ArrayList normalIndicies = new ArrayList(); + for (int n = 1; n < data.length; n++) { + String elements[] = data[n].split("/"); + if (elements.length >= 1) { + // 頂点座標インデックス + coordinateIndicies.add(Integer.parseInt(elements[0]) - 1); + if (elements.length >= 2) { + // テクスチャ座標インデックス + if (elements[0].length() > 0) { + // "v//vn"の場合もあるため + texCoordIndicies.add(Integer.parseInt(elements[1]) - 1); + } + if (elements.length >= 3) { + // 法線ベクトルインデックス + normalIndicies.add(Integer.parseInt(elements[2]) - 1); + } + } + } + } + subGroupCoordinateIndicies.add(coordinateIndicies); + subGroupTexCoordIndicies.add(texCoordIndicies); + subGroupNormalIndicies.add(normalIndicies); + } + } + + // ファイル読み込み後の処理 + if (groups.size() > 0 || subGroups.size() > 0 || subGroupCoordinateIndicies.size() > 0) { + // ストリップの残り、サブグループの残り、グループの残りを最後のオブジェクトとして追加 + Model3D newObject = createRemainingObject(objectName, groups, + groupName, objCoordinates, objTexCoordinates, objNormals, subGroups, + subGroupName, subGroupAp, subGroupCoordinateIndicies, subGroupTexCoordIndicies, subGroupNormalIndicies); + objects.add(newObject); + } + br.close(); + + if (objects.size() == 0) { + System.out.println("No polygon is included"); + throw new ModelFileFormatException(); + } else if (objects.size() == 1) { + if (groups.size() == 0) { + System.out.println("No polygon is included"); + throw new ModelFileFormatException(); + } else if (groups.size() == 1) { + // グループがただ一つの場合 + return groups.get(0); + } else { + // オブジェクトがただ一つの場合 + return objects.get(0); + } + } else { + // オブジェクトが複数の場合 + return new ContainerModel(fileName, (Model3D [])objects.toArray(new Model3D []{})); + } + } + + /** + * グループの残りを新しいオブジェクトとして追加 + * @param objectName 新しいオブジェクトの名前 + * @param groups 新しいオブジェクトを構成するグループ + * @param groupName + * @param groupCoordinates + * @param groupTexCoordinates + * @param groupNormals + * @param subGroups + * @param subGroupName + * @param subGroupAp + * @param subGroupCoordinateIndicies + * @param subGroupTexCoordIndicies + * @param subGroupNormalIndicies + * @return + */ + private static Model3D createRemainingObject(String objectName, ArrayList groups, + String groupName, ArrayList groupCoordinates, ArrayList groupTexCoordinates, + ArrayList groupNormals, ArrayList subGroups, + String subGroupName, Appearance subGroupAp, + ArrayList> subGroupCoordinateIndicies, + ArrayList> subGroupTexCoordIndicies, + ArrayList> subGroupNormalIndicies) { + if (subGroups.size() > 0 || subGroupCoordinateIndicies.size() > 0) { + // ストリップの残り、サブグループの残りを新しいグループとして追加 + Model3D newGroup = createRemainingGroup(groupName, groupCoordinates, groupTexCoordinates, groupNormals, subGroups, + subGroupName, subGroupAp, subGroupCoordinateIndicies, subGroupTexCoordIndicies, subGroupNormalIndicies); + groups.add(newGroup); + subGroups.clear(); + } + + // グループの残りを新しいオブジェクトとして作成 + Model3D newObject = new ContainerModel(objectName, (Model3D [])groups.toArray(new Model3D []{})); + return newObject; + } + + /** + * サブグループの残りを新しいグループとして作成 + * @param groupName 新しいグループの名前 + * @param groupCoordinates 頂点座標の値のリスト + * @param groupTexCoordinates テクスチャ座標の値のリスト + * @param groupNormals 法線ベクトルの値のリスト + * @param subGroups 新しいグループを構成するサブグループ + * @param subGroupName + * @param subGroupAp + * @param subGroupCoordinateIndicies + * @param subGroupTexCoordIndicies + * @param subGroupNormalIndicies + * @return + */ + private static Model3D createRemainingGroup(String groupName, + ArrayList groupCoordinates, ArrayList groupTexCoordinates, ArrayList groupNormals, + ArrayList subGroups, + String subGroupName, Appearance subGroupAp, + ArrayList> subGroupCoordinateIndicies, + ArrayList> subGroupTexCoordIndicies, + ArrayList> subGroupNormalIndicies) { + if (subGroupCoordinateIndicies.size() > 0) { + // ストリップの残りを新しいサブグループとして追加 + Model3D newSubGroup = createRemainingSubGroup(groupCoordinates, groupTexCoordinates, groupNormals, + subGroupName, subGroupAp, subGroupCoordinateIndicies, subGroupTexCoordIndicies, subGroupNormalIndicies); + subGroups.add(newSubGroup); + subGroupCoordinateIndicies.clear(); + subGroupTexCoordIndicies.clear(); + subGroupNormalIndicies.clear(); + } + + // サブグループの残りを新しいグループとして作成 + return new ContainerModel(groupName, (Model3D [])subGroups.toArray(new Model3D []{})); + } + + /** + * ストリップの残りを新しいサブグループとして作成 + * @param groupCoordinates + * @param groupTexCoordinates + * @param groupNormals + * @param subGroupName 新しいサブグループの名前 + * @param subGroupAp 新しいサブグループの表面属性 + * @param subGroupCoordinateIndicies 頂点座標へのインデックス + * @param subGroupTexCoordIndicies テクスチャ座標へのインデックス + * @param subGroupNormalIndicies 法線ベクトルへのインデックス + * @return + */ + private static Model3D createRemainingSubGroup(ArrayList groupCoordinates, + ArrayList groupTexCoordinates, ArrayList groupNormals, + String subGroupName, Appearance subGroupAp, ArrayList> subGroupCoordinateIndicies, + ArrayList> subGroupTexCoordIndicies, + ArrayList> subGroupNormalIndicies) { + // ストリップの残りを新しいサブグループとして作成 + // Geometry の作成 + GeometryArray geometry = createGeometryArray(groupCoordinates, groupTexCoordinates, groupNormals, + subGroupCoordinateIndicies, subGroupTexCoordIndicies, subGroupNormalIndicies); + // Object3D の作成 + return new LeafModel(subGroupName, geometry, subGroupAp); + } + + /** + * ストリップのインデックス情報からジオメトリーを作成 + * @param groupCoordinates 頂点座標の値のリスト + * @param groupTexCoordinates テクスチャ座標の値のリスト + * @param groupNormals 法線ベクトルの値のリスト + * @param subGroupCoordinateIndicies 頂点座標へのインデックス + * @param subGroupTexCoordIndicies テクスチャ座標へのインデックス + * @param subGroupNormalIndicies 法線ベクトルへのインデックス + * @return 作成したジオメトリー + */ + private static GeometryArray createGeometryArray( + ArrayList groupCoordinates, + ArrayList groupTexCoordinates, + ArrayList groupNormals, + ArrayList> subGroupCoordinateIndicies, + ArrayList> subGroupTexCoordIndicies, + ArrayList> subGroupNormalIndicies) { + int vertexCount = 0; + int indexCount = 0; + for (int n = 0; n < subGroupCoordinateIndicies.size(); n++) { + ArrayList coordinateIndicies = subGroupCoordinateIndicies.get(n); + vertexCount += coordinateIndicies.size(); + indexCount += (coordinateIndicies.size() - 2) * 3; + } + int vertexFormat = IndexedGeometryArray.COORDINATES; + if (subGroupTexCoordIndicies.size() > 0) { + vertexFormat |= IndexedGeometryArray.TEXTURE_COORDINATE_2; + } + if (subGroupNormalIndicies.size() > 0) { + vertexFormat |= IndexedGeometryArray.NORMALS; + } + IndexedGeometryArray geometry = new IndexedTriangleArray(vertexCount, vertexFormat, indexCount); + int vertexNum = 0; + int index = 0; + for (int n = 0; n < subGroupCoordinateIndicies.size(); n++) { + ArrayList coordinateIndicies = subGroupCoordinateIndicies.get(n); + ArrayList texCoordIndicies = subGroupTexCoordIndicies.get(n); + ArrayList normalIndicies = subGroupNormalIndicies.get(n); + Vector3f c = groupCoordinates.get(coordinateIndicies.get(0)); + geometry.setCoordinate(vertexNum, new float[]{c.x, c.y, c.z}); + c = groupCoordinates.get(coordinateIndicies.get(1)); + geometry.setCoordinate(vertexNum + 1, new float[]{c.x, c.y, c.z}); + if (texCoordIndicies.size() > 0) { + Vector2f t = groupTexCoordinates.get(texCoordIndicies.get(0)); + geometry.setTextureCoordinate(vertexNum, new float[]{t.x, t.y}); + t = groupTexCoordinates.get(texCoordIndicies.get(1)); + geometry.setTextureCoordinate(vertexNum + 1, new float[]{t.x, t.y}); + } + if (normalIndicies.size() > 0) { + Vector3f nr = groupNormals.get(normalIndicies.get(0)); + geometry.setNormal(vertexNum, nr); + nr = groupNormals.get(normalIndicies.get(1)); + geometry.setNormal(vertexNum + 1, nr); + } + int firstCount = vertexNum; + vertexNum += 2; + for (int i = 2; i < coordinateIndicies.size(); i++) { + c = groupCoordinates.get(coordinateIndicies.get(i)); + geometry.setCoordinate(vertexNum, new float[]{c.x, c.y, c.z}); + geometry.setCoordinateIndex(index, firstCount); + geometry.setCoordinateIndex(index + 1, vertexNum - 1); + geometry.setCoordinateIndex(index + 2, vertexNum); + if (texCoordIndicies.size() > 0) { + Vector2f t = groupTexCoordinates.get(texCoordIndicies.get(i)); + geometry.setTextureCoordinate(vertexNum, new float[]{t.x, t.y}); + geometry.setTextureCoordinateIndex(index, firstCount); + geometry.setTextureCoordinateIndex(index + 1, vertexNum - 1); + geometry.setTextureCoordinateIndex(index + 2, vertexNum); + } + if (normalIndicies.size() > 0) { + Vector3f nr = groupNormals.get(normalIndicies.get(i)); + geometry.setNormal(vertexNum, nr); + geometry.setNormalIndex(index, firstCount); + geometry.setNormalIndex(index + 1, vertexNum - 1); + geometry.setNormalIndex(index + 2, vertexNum); + } + vertexNum += 1; + index += 3; + } + } + return geometry; +// ArrayList coordinateIndiciesMap = new ArrayList(); +// ArrayList texCoordIndiciesMap = new ArrayList(); +// ArrayList normalIndiciesMap = new ArrayList(); +// int indexCount = 0; +// int[] stripIndexCounts = new int[subGroupCoordinateIndicies.size()]; +// for (int n = 0; n < subGroupCoordinateIndicies.size(); n++) { +// ArrayList coordinateIndicies = subGroupCoordinateIndicies.get(n); +// ArrayList texCoordIndicies = subGroupTexCoordIndicies.get(n); +// ArrayList normalIndicies = subGroupNormalIndicies.get(n); +// indexCount += coordinateIndicies.size(); +// stripIndexCounts[n] = coordinateIndicies.size(); +// for (int i = 0; i < coordinateIndicies.size(); i++) { +// int c = coordinateIndicies.get(i); +// int coordinateIndex = coordinateIndiciesMap.indexOf(c); +// if (coordinateIndex == -1) { +// coordinateIndex = coordinateIndiciesMap.size(); +// coordinateIndiciesMap.add(c); +// } +// coordinateIndicies.set(i, coordinateIndex); +// if (texCoordIndicies.size() > 0) { +// int t = texCoordIndicies.get(i); +// int texCoordIndex = texCoordIndiciesMap.indexOf(t); +// if (texCoordIndex == -1) { +// texCoordIndex = texCoordIndiciesMap.size(); +// texCoordIndiciesMap.add(t); +// } +// texCoordIndicies.set(i, texCoordIndex); +// } +// if (normalIndicies.size() > 0) { +// int nr = normalIndicies.get(i); +// int normalIndex = normalIndiciesMap.indexOf(nr); +// if (normalIndex == -1) { +// normalIndex = normalIndiciesMap.size(); +// normalIndiciesMap.add(nr); +// } +// normalIndicies.set(i, normalIndex); +// } +// } +// } +// int vertexCount = coordinateIndiciesMap.size(); +// int vertexFormat = IndexedGeometryArray.COORDINATES; +// if (texCoordIndiciesMap.size() > 0) { +// if (vertexCount < texCoordIndiciesMap.size()) vertexCount = texCoordIndiciesMap.size(); +// vertexFormat |= IndexedGeometryArray.TEXTURE_COORDINATE_2; +// } +// if (normalIndiciesMap.size() > 0) { +// if (vertexCount < normalIndiciesMap.size()) vertexCount = normalIndiciesMap.size(); +// vertexFormat |= IndexedGeometryArray.NORMALS; +// } +// IndexedGeometryArray geometry = new IndexedTriangleFanArray(vertexCount, vertexFormat, indexCount, stripIndexCounts); +// for (int n = 0; n < coordinateIndiciesMap.size(); n++) { +// Vector3f c = groupCoordinates.get(coordinateIndiciesMap.get(n)); +// geometry.setCoordinate(n, new float[]{c.x, c.y, c.z}); +// } +// for (int n = 0; n < texCoordIndiciesMap.size(); n++) { +// Vector2f t = groupTexCoordinates.get(texCoordIndiciesMap.get(n)); +// geometry.setTextureCoordinate(n, new float[]{t.x, t.y}); +// } +// for (int n = 0; n < normalIndiciesMap.size(); n++) { +// Vector3f nr = groupNormals.get(normalIndiciesMap.get(n)); +// geometry.setNormal(n, nr); +// } +// int index = 0; +// for (int n = 0; n < subGroupCoordinateIndicies.size(); n++) { +// ArrayList coordinateIndicies = subGroupCoordinateIndicies.get(n); +// ArrayList texCoordIndicies = subGroupTexCoordIndicies.get(n); +// ArrayList normalIndicies = subGroupNormalIndicies.get(n); +// for (int i = 0; i < coordinateIndicies.size(); i++) { +// geometry.setCoordinateIndex(index, coordinateIndicies.get(i)); +// if (texCoordIndicies.size() > 0) { +// geometry.setTextureCoordinateIndex(index, texCoordIndicies.get(i)); +// } +// if (normalIndicies.size() > 0) { +// geometry.setNormalIndex(index, normalIndicies.get(i)); +// } +// index++; +// } +// } +// return geometry; + } + + /** + * MTLファイルの読み込み + * @param fileName ファイル名 + * @return 材質名から表面属性オブジェクトへのマッピング + * @throws IOException + * @throws ModelFileFormatException + */ + private static HashMap loadMtlFile(String fileName) throws IOException, ModelFileFormatException { + File file = new File(fileName); + FileReader filereader = new FileReader(file); + BufferedReader br = new BufferedReader(filereader); + + HashMap appearances = new HashMap(); + String materialName; + Appearance ap = null; + Material m = null; + + String line = null; + while ((line = br.readLine()) != null) { + line.trim(); + String data[] = line.split(" "); + if (data[0].equals("newmtl")) { + if (data.length < 1) { + System.out.println("Expected material's name"); + throw new ModelFileFormatException(); + } + materialName = data[1]; + m = new Material(); + ap = new Appearance(); + ap.setMaterial(m); + appearances.put(materialName, ap); + } else if (data[0].equals("Kd")) { + if (data.length < 4) { + System.out.println("Expected diffuse color"); + throw new ModelFileFormatException(); + } + float r = Float.parseFloat(data[1]); + float g = Float.parseFloat(data[2]); + float b = Float.parseFloat(data[3]); + m.setDiffuseColor(r, g, b); + } else if (data[0].equals("Ka")) { + if (data.length < 4) { + System.out.println("Expected ambient color"); + throw new ModelFileFormatException(); + } + float r = Float.parseFloat(data[1]); + float g = Float.parseFloat(data[2]); + float b = Float.parseFloat(data[3]); + m.setAmbientColor(r, g, b); + } else if (data[0].equals("Ks")) { + if (data.length < 4) { + System.out.println("Expected specular color"); + throw new ModelFileFormatException(); + } + float r = Float.parseFloat(data[1]); + float g = Float.parseFloat(data[2]); + float b = Float.parseFloat(data[3]); + m.setSpecularColor(r, g, b); + } else if (data[0].equals("Tr")) { + if (data.length < 4) { + System.out.println("Expected emissive color"); + throw new ModelFileFormatException(); + } + float r = Float.parseFloat(data[1]); + float g = Float.parseFloat(data[2]); + float b = Float.parseFloat(data[3]); + m.setEmissiveColor(r, g, b); + } else if (data[0].equals("Ns")) { + if (data.length < 2) { + System.out.println("Expected shiness value"); + throw new ModelFileFormatException(); + } + float s = Float.parseFloat(data[1]); + m.setShininess(s); + } else if (data[0].equals("map_Kd")) { + if (data.length < 2) { + System.out.println("Expected texture file's name"); + throw new ModelFileFormatException(); + } + String dir = new File(fileName).getParent(); + String texFileName = line.substring(line.indexOf(" ") + 1); + String texFilePath = new File(dir, texFileName).getPath(); + TextureLoader texLoader = new TextureLoader(texFilePath, TextureLoader.BY_REFERENCE | TextureLoader.Y_UP, null); + ap.setTexture(texLoader.getTexture()); + } + } + return appearances; + } + + private static Integer readInt(BufferedInputStream bis) throws IOException { + byte[] intBin = new byte[4]; + if (bis.read(intBin, 3, 1) < 1) { + return null; + } + if (bis.read(intBin, 2, 1) < 1) { + return null; + } + if (bis.read(intBin, 1, 1) < 1) { + return null; + } + if (bis.read(intBin, 0, 1) < 1) { + return null; + } + return ByteBuffer.wrap(intBin).getInt(); + } + + private static Float readFloat(BufferedInputStream bis) throws IOException { + byte[] floatBin = new byte[4]; + if (bis.read(floatBin, 3, 1) < 1) { + return null; + } + if (bis.read(floatBin, 2, 1) < 1) { + return null; + } + if (bis.read(floatBin, 1, 1) < 1) { + return null; + } + if (bis.read(floatBin, 0, 1) < 1) { + return null; + } + return ByteBuffer.wrap(floatBin).getFloat(); + } +} diff --git a/src/main/java/framework/model3D/ModelFileFormatException.java b/src/main/java/framework/model3D/ModelFileFormatException.java new file mode 100644 index 0000000..03dc0e3 --- /dev/null +++ b/src/main/java/framework/model3D/ModelFileFormatException.java @@ -0,0 +1,5 @@ +package framework.model3D; + +public class ModelFileFormatException extends Exception { + +} diff --git a/src/main/java/framework/model3D/Movable.java b/src/main/java/framework/model3D/Movable.java new file mode 100644 index 0000000..bf91f2b --- /dev/null +++ b/src/main/java/framework/model3D/Movable.java @@ -0,0 +1,8 @@ +package framework.model3D; + +import framework.model3D.Placeable; +import framework.physics.Ground; + +public interface Movable extends Placeable { + public void motion(long interval, Ground ground); +} diff --git a/src/main/java/framework/model3D/OBB.java b/src/main/java/framework/model3D/OBB.java new file mode 100644 index 0000000..912f4db --- /dev/null +++ b/src/main/java/framework/model3D/OBB.java @@ -0,0 +1,617 @@ +package framework.model3D; + +import javax.media.j3d.BoundingPolytope; +import javax.media.j3d.BoundingSphere; +import javax.media.j3d.Transform3D; +import javax.vecmath.Matrix4d; +import javax.vecmath.Point3d; +import javax.vecmath.Vector3d; +import javax.vecmath.Vector4d; +import java.util.ArrayList; +import java.util.Arrays; + +public class OBB implements Cloneable { + private ArrayList vertexList = new ArrayList(); + private Vector4d[] plane; + private BoundingPolytope bp = null; + private static int edges[][] = { { 0, 1 }, { 0, 2 }, { 1, 3 }, { 2, 3 }, + { 0, 4 }, { 1, 5 }, { 3, 7 }, { 2, 6 }, + { 4, 5 }, { 4, 6 }, { 5, 7 }, { 6, 7 } }; + private static Integer planes[][] = { {0, 2, 3, 1}, {0, 1, 5, 4}, + {1, 3, 7, 5}, {4, 5, 7, 6}, + {0, 4, 6, 2}, {2, 6, 7, 3}}; + private static boolean[][] inside = new boolean[8][6]; + + public OBB() { + } + + public OBB(Vector3d v1, Vector3d v2, Vector3d v3, Vector3d v4, + Vector3d v5, Vector3d v6, Vector3d v7, Vector3d v8) { + vertexList.add(v1); + vertexList.add(v2); + vertexList.add(v3); + vertexList.add(v4); + vertexList.add(v5); + vertexList.add(v6); + vertexList.add(v7); + vertexList.add(v8); + createPlanes(); + } + + public void addVertex(Vector3d v) { + vertexList.add(v); + } + + public Vector3d getVertex(int i) { + return vertexList.get(i); + } + + public BoundingPolytope getBoundingPolytope() { + return bp; + } + + public BoundingSphere getBoundingSphere() { + double radius = 0.0; + Point3d p = new Point3d(); + Vector3d cv = new Vector3d(); + for (int i = 0; i < vertexList.size() - 1; i++) { + for (int j = i + 1; j < vertexList.size(); j++) { + Vector3d v = new Vector3d(); + v.sub(vertexList.get(i), vertexList.get(j)); + if (radius < v.length()) { + radius = v.length(); + cv.add(vertexList.get(i), vertexList.get(j)); + cv.scale(0.5); + p.x = cv.x; + p.y = cv.y; + p.z = cv.z; + } + } + } + BoundingSphere s = new BoundingSphere(p, radius / 2); + return s; + } + + /** + * 頂点情報を元に面および境界多面体を生成する + */ + public void createPlanes() { + // 面の作成 + Vector3d v1 = new Vector3d(); + Vector3d v2 = new Vector3d(); + Vector3d v3 = new Vector3d(); + Vector4d[] plane = new Vector4d[6]; + + // 0231 + v1 = getVertex(0); + v2.sub(getVertex(2), getVertex(0)); + v3.sub(getVertex(1), getVertex(0)); + Vector3d n = new Vector3d(); + n.cross(v2, v3); + n.normalize(); + plane[0] = new Vector4d(); + plane[0].set(n.x, n.y, n.z, -n.dot(v1)); + + + // 0154 + v1 = getVertex(0); + v2.sub(getVertex(1), getVertex(0)); + v3.sub(getVertex(4), getVertex(0)); + n = new Vector3d(); + n.cross(v2, v3); + n.normalize(); + plane[1] = new Vector4d(); + plane[1].set(n.x, n.y, n.z, -n.dot(v1)); + + // 1375 + v1 = getVertex(1); + v2.sub(getVertex(3), getVertex(1)); + v3.sub(getVertex(5), getVertex(1)); + n = new Vector3d(); + n.cross(v2, v3); + n.normalize(); + plane[2] = new Vector4d(); + plane[2].set(n.x, n.y, n.z, -n.dot(v1)); + + // 4576 + v1 = getVertex(6); + v2.sub(getVertex(4), getVertex(6)); + v3.sub(getVertex(7), getVertex(6)); + n = new Vector3d(); + n.cross(v2, v3); + n.normalize(); + plane[3] = new Vector4d(); + plane[3].set(n.x, n.y, n.z, -n.dot(v1)); + + // 0462 + v1 = getVertex(6); + v2.sub(getVertex(2), getVertex(6)); + v3.sub(getVertex(4), getVertex(6)); + n = new Vector3d(); + n.cross(v2, v3); + n.normalize(); + plane[4] = new Vector4d(); + plane[4].set(n.x, n.y, n.z, -n.dot(v1)); + + // 2673 + v1 = getVertex(6); + v2.sub(getVertex(7), getVertex(6)); + v3.sub(getVertex(2), getVertex(6)); + n = new Vector3d(); + n.cross(v2, v3); + n.normalize(); + plane[5] = new Vector4d(); + plane[5].set(n.x, n.y, n.z, -n.dot(v1)); + + this.plane = plane; + + bp = new BoundingPolytope(); + bp.setPlanes(plane); + } + + /** + * 平面との衝突判定 + * @param plane 衝突判定の対象となる平面 + * @return 衝突判定の結果(衝突していない場合はnull) + */ + public CollisionResult intersect(Vector4d plane) { + int i = 0; + boolean inside = false; + boolean outside = false; + int count = 0; + Vector3d center = new Vector3d(0, 0, 0); + ArrayList collisionPoints = new ArrayList(); + double l = Math.sqrt(plane.x * plane.x + plane.y * plane.y + plane.z * plane.z); + double length; + double deepest = 0.0; + Vector3d v; + for (i = 0; i < vertexList.size(); i++) { + v = vertexList.get(i); + if (GeometryUtility.inside(v, plane)) { + inside = true; + length = -(v.x * plane.x + v.y * plane.y + v.z * plane.z + plane.w) / l; + if (length > deepest + GeometryUtility.TOLERANCE) { + center.set(v); + collisionPoints.clear(); + collisionPoints.add(v); + count = 1; + deepest = length; + } else if (length >= deepest - GeometryUtility.TOLERANCE) { + center.add(v); + collisionPoints.add(v); + count++; + } + } else { + outside = true; + } + } + + if (!inside || !outside) { + // 全頂点が外側か全頂点が内側の場合 + return null; + } + + center.scale(1.0 / count); + + CollisionResult cr = new CollisionResult(); + cr.length = deepest; + cr.collisionPoint.setVector3d(center); + cr.collisionPoints = collisionPoints; + cr.normal.setX(plane.x / l); + cr.normal.setY(plane.y / l); + cr.normal.setZ(plane.z / l); + + return cr; + } + + /** + * OBBとの衝突判定 + * @param another 衝突判定の対象となるOBB + * @return 衝突判定の結果(衝突していない場合はnull) + */ + public CollisionResult intersect(OBB another) { + // anotherの各頂点がthisの各面の内側にあるかを調べる + // i:anotherの頂点, j:thisの面 + OBB o1 = this; + OBB o2 = another; + for (int i = 0; i < o2.vertexList.size(); i++) { + for (int j = 0; j < o1.plane.length; j++) { + inside[i][j] = GeometryUtility.inside(o2.vertexList.get(i), o1.plane[j]); + } + } + + int v = 0; + boolean bCollides = false; + + // anotherの各頂点がthisに包含されているか? + Vector3d contactCenter = new Vector3d(); + int count = 0; + for (int i = 0; i < o2.vertexList.size(); i++) { + boolean f1 = false; + for (int p = 0; p < o1.plane.length; p++) { + f1 = inside[i][p]; + if (!f1) break; // 包含されていない + } + if (f1) { + // thisのすべての面の内側 = thisに包含されている + if (!bCollides) { + bCollides = true; + v = i; + } + contactCenter.add(o2.vertexList.get(i)); + count++; + } + } + if (!bCollides) { + // thisの各頂点がanotherの各面の内側にあるかを調べる + o1 = another; + o2 = this; + for (int i = 0; i < o2.vertexList.size(); i++) { + for (int j = 0; j < o1.plane.length; j++) { + inside[i][j] = GeometryUtility.inside(o2.vertexList.get(i), o1.plane[j]); + } + } + // thisの各頂点がanotherに包含されているか? + for (int i = 0; i < o2.vertexList.size(); i++) { + boolean f1 = false; + for (int p = 0; p < o1.plane.length; p++) { + f1 = inside[i][p]; + if (!f1) break; // 包含されていない + } + if (f1) { + // anotherのすべての面の内側 = anotherに包含されている + if (!bCollides) { + bCollides = true; + v = i; + } + contactCenter.add(o2.vertexList.get(i)); + count++; + } + } + } + + CollisionResult cr = new CollisionResult(); + ArrayList collisionPoints = new ArrayList(); + if (bCollides) { + // いずれかのOBBの頂点vが他方のOBBに包含されていた場合(vを持つ側のOBBをo2、他方をo1とする) + // o1のどの面と衝突したかを判定する + double lMin = 0; + int p1 = -1; + Vector3d normal = null; + contactCenter.scale(1.0 / (double)count); + for (int p = 0; p < o1.plane.length; p++) { + Vector3d n = new Vector3d(o1.plane[p].x, o1.plane[p].y, o1.plane[p].z); + double l = -(contactCenter.x * o1.plane[p].x + contactCenter.y * o1.plane[p].y + + contactCenter.z * o1.plane[p].z + o1.plane[p].w) / n.length(); + if (lMin > l || normal == null) { + lMin = l; + p1 = p; + normal = n; + } + } + + // o2上のvを含む辺のうち衝突面の法線と逆方向に向かっている辺を探す(そのような辺の数によって接触状況が変わる) + Vector3d collisionPoint = o2.vertexList.get(v); + Vector3d d1; + ArrayList contactEdges = new ArrayList(); + for (int e = 0; e < edges.length; e++) { + if (v == edges[e][0]) { + d1 = new Vector3d(o2.vertexList.get(edges[e][1])); + d1.sub(collisionPoint); + if (normal.dot(d1) < GeometryUtility.TOLERANCE) { + // 衝突面に接している辺 + contactEdges.add(edges[e][1]); // vの反対側の端点 + } + } else if (v == edges[e][1]) { + d1 = new Vector3d(o2.vertexList.get(edges[e][0])); + d1.sub(collisionPoint); + if (normal.dot(d1) < GeometryUtility.TOLERANCE) { + // 衝突面に接している辺 + contactEdges.add(edges[e][0]); // vの反対側の端点 + } + } + } + + // 接触状況に応じた処理 + if (contactEdges.size() == 0) { + // a) 頂点で接触 + collisionPoints.add(collisionPoint); + cr.collisionPoint.setVector3d(collisionPoint); + } else if (contactEdges.size() == 1) { + // b) 辺で接触 + boolean f1 = true; + Vector3d otherPoint = o2.vertexList.get(contactEdges.get(0)); + Vector3d intersectionPoint = null; + double nearest = 0.0; + for (int p = 0; p < o1.plane.length; p++) { + if (!inside[contactEdges.get(0)][p]) { + // collisionPoint と otherPoint の間を通る平面 + Vector3d ip = GeometryUtility.intersect(o1.plane[p], collisionPoint, otherPoint); + if (ip != null) { + f1 = false; // 包含されていない + Vector3d ip2 = (Vector3d)ip.clone(); + ip2.sub(collisionPoint); + double d = ip2.length(); + if (intersectionPoint == null || d < nearest) { + intersectionPoint = ip; + nearest = d; + } + } + } + } + if (f1) { + // b-1) 辺全体が接触している + collisionPoints.add(collisionPoint); + collisionPoints.add(otherPoint); + Vector3d center = new Vector3d(collisionPoint); + center.add(otherPoint); + center.scale(0.5); + cr.collisionPoint.setVector3d(center); + } else { + // b-2) 辺の一部が接触している + collisionPoints.add(collisionPoint); + collisionPoints.add(intersectionPoint); + Vector3d center = new Vector3d(collisionPoint); + center.add(intersectionPoint); + center.scale(0.5); + cr.collisionPoint.setVector3d(center); + } + } else { + // c) 面で接触 + Vector3d center = new Vector3d(); + int v2 = contactEdges.get(0); + int v3 = contactEdges.get(1); + int p2; + for (p2 = 0; p2 < o2.plane.length; p2++) { + if (v == planes[p2][0] || v == planes[p2][1] || v == planes[p2][2] || v == planes[p2][3]) { + if (v2 == planes[p2][0] || v2 == planes[p2][1] || v2 == planes[p2][2] || v2 == planes[p2][3]) { + if (v3 == planes[p2][0] || v3 == planes[p2][1] || v3 == planes[p2][2] || v3 == planes[p2][3]) { + break; + } + } + } + } + // o1の面p1とo2の面p2が接している + // 面と面の共通領域を求める + for (v2 = 0; v2 < 4; v2++) { + if (planes[p2][v2] == v) break; + } + Vector3d d2 = (Vector3d)o2.vertexList.get(planes[p2][(v2 + 1) % 4]).clone(); + d2.sub(collisionPoint); + d2.cross(normal, d2); + int sign = -1; + int v1; + for (v1 = 4; v1 >= 0; v1--) { + d1 = (Vector3d)o1.vertexList.get(planes[p1][v1 % 4]).clone(); + d1.sub(o1.vertexList.get(planes[p1][(v1 + 1) % 4])); + if (d2.dot(d1) > -GeometryUtility.TOLERANCE) { + sign = 1; + } else if (sign == 1) { + // 内積の符号が正から負に変わったとき + v1 = (v1 + 1) % 4; + break; + } + } + Vector3d vp1, vp2, dst1, dst2, src1, src2, d3; + ArrayList contours = new ArrayList(); + for (int i = 0; i < 4; i++) { + dst1 = vp1 = (Vector3d)o1.vertexList.get(planes[p1][(v1 - i + 4) % 4]).clone(); + src2 = vp2 = (Vector3d)o2.vertexList.get(planes[p2][(v2 + i) % 4]).clone(); + src1 = (Vector3d)o1.vertexList.get(planes[p1][(v1 - i + 4 + 1) % 4]).clone(); + dst2 = (Vector3d)o2.vertexList.get(planes[p2][(v2 + i + 1) % 4]).clone(); + d1 = (Vector3d)dst1.clone(); + d1.sub(src1); + d2 = (Vector3d)dst2.clone(); + d2.sub(src2); + d3 = (Vector3d)vp2.clone(); + d3.sub(vp1); + d1.cross(d1, normal); + if (d1.dot(d3) < GeometryUtility.TOLERANCE) { + // o1の辺は、頂点vp2の内側にあるので、共通領域の輪郭を構成する + contours.add(new Vector3d[]{src1, dst1}); + } + d3.negate(); + d2.cross(d2, normal); + if (d2.dot(d3) < GeometryUtility.TOLERANCE) { + // o2の辺は、頂点vp1の内側にあるので、共通領域の輪郭を構成する + contours.add(new Vector3d[]{src2, dst2}); + } + } + Vector3d[] edge1, edge2; + Vector3d d; + Vector4d p; + for (int i = 0; i < contours.size(); i++) { + edge1 = contours.get(i); + edge2 = contours.get((i + 1) % contours.size()); + if (edge1[1].equals(edge2[0])) { + // edge1とedge2は始点と終点を共有している + if (!collisionPoints.contains(edge1[1])) { + collisionPoints.add(edge1[1]); + center.add(edge1[1]); + } + } else if (!edge1[0].equals(edge2[0]) && !edge1[1].equals(edge2[1])) { + // edge1とedge2の交点を求める + d = (Vector3d)edge2[1].clone(); + d.add(normal); + p = GeometryUtility.createPlane(edge2[0], edge2[1], d); + if (p != null) { + d = GeometryUtility.intersect(p, edge1[0], edge1[1]); + if (d != null && !collisionPoints.contains(d)) { + collisionPoints.add(d); + center.add(d); + } + } + } + } + if (collisionPoints.size() <= 2) return null; // 面で接触しているはずが2点以下である場合、衝突していない + center.scale(1.0 / (double)collisionPoints.size()); + cr.collisionPoint.setVector3d(center); + } + cr.length = lMin; + cr.collisionPoints = collisionPoints; + if (o2 == this) normal.negate(); + normal.normalize(); + cr.normal = normal; + return cr; + } else { + // anotherの辺がthisの面と交わっているか? + Vector3d center = new Vector3d(); + Vector3d normal = new Vector3d(); + ArrayList intersections = new ArrayList(); + ArrayList contactPlanes = new ArrayList(); + for (int e = 0; e < edges.length; e++) { + // anotherの各辺eに対して処理 + Vector3d intersection = null; + intersections.clear(); + contactPlanes.clear(); + for (int p = 0; p < this.plane.length; p++) { + if (inside[edges[e][0]][p] == !inside[edges[e][1]][p]) { + // anotherの辺eが、thisの面pを貫いている + Vector3d ip = GeometryUtility.intersect( + this.plane[p], + another.vertexList.get(edges[e][0]), + another.vertexList.get(edges[e][1])); + // eとpの交点がthisに包含されているか? + if (ip != null) { + intersection = ip; + boolean bInside = true; + for (int p2 = 0; p2 < 6; p2++) { + if (p != p2 + && !GeometryUtility.inside(intersection, this.plane[p2])) { + bInside = false; + break; + } + } + if (bInside) { + // eがpと交わっている + contactPlanes.add(p); + intersections.add(intersection); + bCollides = true; + } + } + } + } + if (contactPlanes.size() == 1) { + // eと交わっているthisの面が1つ + collisionPoints.add(intersection); + center.add(intersection); + } else if (contactPlanes.size() == 2) { + // eと交わっているthisの面が2つ + int p1 = contactPlanes.get(0); + int p2 = contactPlanes.get(1); + ArrayList p1vs = new ArrayList(Arrays.asList(planes[p1])); + ArrayList p2vs = new ArrayList(Arrays.asList(planes[p2])); + p1vs.retainAll(p2vs); + if (p1vs.size() == 2) { + // p1とp2は隣り合う面(anotherの辺eはthisのある辺と接触している) + Vector3d v1 = this.vertexList.get(p1vs.get(0)); // p1とp2の間の辺の始点 + Vector3d v2 = this.vertexList.get(p1vs.get(1)); // p1とp2の間の辺の終点 + Vector3d d1 = (Vector3d)v1.clone(); + d1.sub(v2); + Vector3d d2 = (Vector3d)another.vertexList.get(edges[e][0]).clone(); + d2.sub(another.vertexList.get(edges[e][1])); + normal.cross(d1, d2); // 辺eと、p1とp2の間の辺の両方に垂直なベクトルを法線方向とする + normal.normalize(); + Vector3d n1 = new Vector3d(this.plane[p1].x, this.plane[p1].y, this.plane[p1].z); + Vector3d n2 = new Vector3d(this.plane[p2].x, this.plane[p2].y, this.plane[p2].z); + n1.normalize(); + n2.normalize(); + n1.add(n2); + n1.scale(0.5); + if (n1.dot(normal) < -GeometryUtility.TOLERANCE) { + normal.negate(); + } + intersection = intersections.get(0); + intersection.add(intersections.get(1)); + intersection.scale(0.5); + center.add(intersection); // 衝突点は1つ + collisionPoints.add(intersection); + Vector3d c = GeometryUtility.nearest(v1, v2, center); + c.sub(center); + cr.length = c.length(); + cr.normal = normal; + } else { + // p1とp2は向かい合う面(anotherの辺eはthisのある面と接触している) + center.add(intersections.get(0)); // 衝突点は2つ + center.add(intersections.get(1)); + collisionPoints.add(intersections.get(0)); + collisionPoints.add(intersections.get(1)); + // thisのどの面と衝突したかを判定する + double lMin = 0; + normal = null; + Vector4d plane; + for (int p = 0; p < this.plane.length; p++) { + plane = this.plane[p]; + Vector3d n = new Vector3d(plane.x, plane.y, plane.z); + double l = -(center.x * plane.x + center.y * plane.y + center.z * plane.z + plane.w) / n.length(); + if (lMin > l || normal == null) { + lMin = l; + normal = n; + } + } + normal.normalize(); + cr.length = lMin; + cr.normal = normal; + } + } + } + if (!bCollides) return null; + center.scale(1.0 / (double)collisionPoints.size()); + cr.collisionPoint.setVector3d(center); + cr.collisionPoints = collisionPoints; + return cr; + } + } + + public Object clone() { + OBB obb = new OBB(); + obb.plane = new Vector4d[6]; + for (int i = 0; i < plane.length; i++) { + obb.plane[i] = (Vector4d) plane[i].clone(); + + } + for (int i = 0; i < vertexList.size(); i++) { + obb.vertexList.add((Vector3d) vertexList.get(i).clone()); + } + obb.bp = new BoundingPolytope(obb.plane); + + // System.out.println("veretexListSIZE:"+vertexList.size()); + return obb; + } + + public void transform(Transform3D t) { + // TODO Auto-generated method stub + bp.transform(t); + bp.getPlanes(plane); + // for(int i = 0; i" + plane[i].x + "," + plane[i].y + "," + + // plane[i].z + "," + plane[i].w); + // } + for (int i = 0; i < vertexList.size(); i++) { + // System.out.println("vertex"); + // System.out.print(vertexList.get(i).x + "," + vertexList.get(i).y + // + "," + vertexList.get(i).z); + Matrix4d mat4d = new Matrix4d(); + t.get(mat4d); + double x = mat4d.m00 * vertexList.get(i).x + mat4d.m01 + * vertexList.get(i).y + mat4d.m02 * vertexList.get(i).z + + mat4d.m03; + double y = mat4d.m10 * vertexList.get(i).x + mat4d.m11 + * vertexList.get(i).y + mat4d.m12 * vertexList.get(i).z + + mat4d.m13; + double z = mat4d.m20 * vertexList.get(i).x + mat4d.m21 + * vertexList.get(i).y + mat4d.m22 * vertexList.get(i).z + + mat4d.m23; + vertexList.get(i).x = x; + vertexList.get(i).y = y; + vertexList.get(i).z = z; + // t.transform(vertexList.get(i)); + // System.out.println("-->" + vertexList.get(i).x + "," + + // vertexList.get(i).y + "," + vertexList.get(i).z); + } + } +} diff --git a/src/main/java/framework/model3D/Object3D.java b/src/main/java/framework/model3D/Object3D.java new file mode 100644 index 0000000..61801de --- /dev/null +++ b/src/main/java/framework/model3D/Object3D.java @@ -0,0 +1,717 @@ +package framework.model3D; + +import com.sun.j3d.utils.geometry.*; + +import javax.media.j3d.*; +import javax.vecmath.AxisAngle4d; +import javax.vecmath.Quat4d; +import javax.vecmath.Vector3d; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; + +/** + * 階層化され名前付けられた基本オブジェクト + * @author 新田直也 + * + */ +public class Object3D extends BaseObject3D { + public Object3D[] children = new Object3D[0]; + public String name; + public TransformGroup pos; + public TransformGroup rot; + public TransformGroup scale; + protected Position3D position = new Position3D(); + protected Quaternion3D quaternion; + + public BoundingSphere bs = null; + private OBB obb = null; + private UndoBuffer undoBuffer = new UndoBuffer(); + private boolean bLOD = false; + private LOD lodNode = null; + + // コンストラクタ + public Object3D(String name, Primitive node) { + if (name == null) name = ""; + this.name = new String(name); + pos = new TransformGroup(); + pos.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + pos.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + rot = new TransformGroup(); + rot.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + rot.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + scale = new TransformGroup(); + scale.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + scale.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + center = new TransformGroup(); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + pos.addChild(rot); + rot.addChild(scale); + scale.addChild(center); + center.addChild(node); + } + + public Object3D(String name, Leaf node) { + if (name == null) name = ""; + this.name = new String(name); + if (!(node instanceof LOD)) { + pos = new TransformGroup(); + pos.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + pos.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + rot = new TransformGroup(); + rot.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + rot.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + scale = new TransformGroup(); + scale.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + scale.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + center = new TransformGroup(); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + pos.addChild(rot); + rot.addChild(scale); + scale.addChild(center); + center.addChild(node); + } else { + // LOD の場合 + bLOD = true; + lodNode = (LOD)node; + pos = new TransformGroup(); + pos.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + pos.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + rot = new TransformGroup(); + rot.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + rot.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + scale = new TransformGroup(); + scale.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + scale.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + center = new TransformGroup(); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + pos.addChild(rot); + rot.addChild(scale); + scale.addChild(center); + BranchGroup lodRoot = new BranchGroup(); + lodRoot.addChild(lodNode); + lodRoot.addChild(lodNode.getSwitch(0)); + center.addChild(lodRoot); + } + } + + public Object3D(String name, Geometry g, Appearance a) { + if (name == null) name = ""; + this.name = new String(name); + pos = new TransformGroup(); + pos.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + pos.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + rot = new TransformGroup(); + rot.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + rot.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + scale = new TransformGroup(); + scale.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + scale.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + center = new TransformGroup(); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + a.setCapability(Appearance.ALLOW_TEXTURE_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_WRITE); + a.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_WRITE); + a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_WRITE); + Shape3D shape = new Shape3D(g, a); + shape.setCapability(Shape3D.ALLOW_APPEARANCE_READ); + shape.setCapability(Shape3D.ALLOW_APPEARANCE_WRITE); + pos.addChild(rot); + rot.addChild(scale); + scale.addChild(center); + center.addChild(shape); + } + + // コンストラクタ + public Object3D(String name, Object3D[] children) { + if (name == null) + name = ""; + this.name = name; + this.children = children; + int n = children.length; + int i = 0; + + pos = new TransformGroup(); + pos.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + pos.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + rot = new TransformGroup(); + rot.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + rot.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + scale = new TransformGroup(); + scale.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + scale.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + center = new TransformGroup(); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + center.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + + pos.addChild(rot); + rot.addChild(scale); + scale.addChild(center); + + for (i = 0; i < n; i++) { + center.addChild(children[i].pos); + if (children[i].isBumpMappingApplied()) bBumpMapApplied = true; + if (children[i].isReflectionMappingApplied()) bReflectionMapApplied = true; + } + } + + public Object3D(String name, Object3D[] children, + Transform3D defaultTransform) { + this(name, children); + if (defaultTransform == null) return; + pos.setTransform(defaultTransform); + } + + // コピーコンストラクタ + public Object3D(Object3D obj) { + this.name = new String(obj.name); + if (obj.position != null) { + this.position = new Position3D(obj.position); + } else { + this.position = new Position3D(); + } + if (obj.getQuaternion() != null) { + this.quaternion = new Quaternion3D(obj.getQuaternion()); + } else { + this.quaternion = new Quaternion3D(); + } + Transform3D transPos = new Transform3D(); + obj.pos.getTransform(transPos); + this.pos = new TransformGroup(transPos); + this.pos.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + this.pos.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + Transform3D transRot = new Transform3D(); + obj.rot.getTransform(transRot); + this.rot = new TransformGroup(transRot); + this.rot.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + this.rot.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + Transform3D transScale = new Transform3D(); + obj.scale.getTransform(transScale); + this.scale = new TransformGroup(transScale); + this.scale.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + this.scale.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + Transform3D transCenter = new Transform3D(); + obj.center.getTransform(transCenter); + this.center = new TransformGroup(transCenter); + this.center.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + this.center.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + this.pos.addChild(this.rot); + this.rot.addChild(this.scale); + this.scale.addChild(this.center); + this.bLOD = obj.bLOD; + if (obj.hasChildren()) { + this.children = new Object3D[obj.children.length]; + for (int i = 0; i < obj.children.length; i++) { + this.children[i] = new Object3D(obj.children[i]); + this.center.addChild(this.children[i].pos); + } + } else { + this.children = new Object3D[0]; + if (!bLOD) { + Enumeration nodes = obj.getPrimitiveNodes(); + while (nodes.hasMoreElements()) { + Node node = nodes.nextElement(); + if (node != null && node instanceof Shape3D) { + Shape3D shape = (Shape3D)node; + shape.setCapability(Shape3D.ALLOW_APPEARANCE_READ); + shape.setCapability(Shape3D.ALLOW_APPEARANCE_WRITE); + Appearance a = (Appearance)shape.getAppearance().cloneNodeComponent(true); + a.setCapability(Appearance.ALLOW_TEXTURE_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_WRITE); + a.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_WRITE); + a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_READ); + a.setCapability(Appearance.ALLOW_TEXTURE_UNIT_STATE_WRITE); + this.center.addChild(new Shape3D((Geometry) shape + .getAllGeometries().nextElement(), a)); + } else if (node != null && node instanceof Primitive) { + Primitive primitive = (Primitive)node; + primitive = (Primitive)primitive.cloneTree(); + primitive.setCapability(Primitive.ENABLE_APPEARANCE_MODIFY); + this.center.addChild(primitive); + } + } + } else { + // LOD の場合 + this.lodNode = (LOD)obj.lodNode.cloneTree(); + BranchGroup lodRoot = new BranchGroup(); + lodRoot.addChild(lodNode); + lodRoot.addChild(lodNode.getSwitch(0)); + this.center.addChild(lodRoot); + } + } + if (obj.obb != null) { + this.obb = obj.obb; + } + if (obj.bs != null) { + this.bs = obj.bs; + } + if (obj.boundingSurfaces != null) { + this.boundingSurfaces = obj.boundingSurfaces; + } + this.undoBuffer = new UndoBuffer(obj.undoBuffer); + } + + public Node getPrimitiveNode() { + if (hasChildren()) return null; + if (!bLOD) { + return (Node)center.getChild(0); + } else { + // LOD の場合 + return lodNode.getSwitch(0).getChild(0); + } + } + + public Enumeration getPrimitiveNodes() { + if (hasChildren()) return null; + if (!bLOD) { + return (Enumeration)center.getAllChildren(); + } else { + // LOD の場合 + return lodNode.getSwitch(0).getAllChildren(); + } + } + + public ArrayList getAppearances() { + ArrayList appearances = new ArrayList(); + if (!bLOD) { + if (!hasChildren()) { + appearances.add(getAppearance()); + } else { + for (int n = 0; n < children.length; n++) { + appearances.addAll(children[n].getAppearances()); + } + } + return appearances; + } else { + // LOD の場合 + Enumeration nodes = getPrimitiveNodes(); + while (nodes.hasMoreElements()) { + Node node = nodes.nextElement(); + Appearance ap = null; + if (node != null && node instanceof Shape3D) { + Shape3D shape = (Shape3D)node; + ap = shape.getAppearance(); + if (ap == null) { + ap = new Appearance(); + shape.setAppearance(ap); + } + } else if (node != null && node instanceof Primitive) { + Primitive primitive = (Primitive)node; + ap = primitive.getAppearance(); + if (ap == null) { + ap = new Appearance(); + primitive.setAppearance(ap); + } + } + appearances.add(ap); + } + return appearances; + } + } + + // 引数part(部分)のオブジェクトを探す + public Object3D getPart(String part) { + + int j = 0; + int N = children.length; + Object3D obj; + + // if(part == children[j].name) return this; + if (part.equals(this.name)) + return this; + + for (j = 0; j < N; j++) { + if (children[j] != null) { + obj = children[j].getPart(part); + if (obj != null) + return obj; + else + continue; + } + } + return null; + } + + @Override + public TransformGroup getTransformGroupToPlace() { + return pos; + } + + // Position3D の applyTo() 以外からは呼ばないこと + void setPosition(Position3D p) { + position = (Position3D) p.clone(); + Vector3d v = new Vector3d(p.getX(), p.getY(), p.getZ()); + Transform3D trans = new Transform3D(); + trans.set(v); + pos.setTransform(trans); + } + + public void scale(double s) { + Transform3D trans = new Transform3D(); + trans.setScale(s); + scale.setTransform(trans); + } + + public void scale(double x, double y, double z) { + Transform3D trans = new Transform3D(); + trans.setScale(new Vector3d(x, y, z)); + scale.setTransform(trans); + } + + public Position3D getPosition3D() { + return (Position3D) position.clone(); + + } + + // キャラクターを回転させる + public void rotate(double vx, double vy, double vz, double a) { + AxisAngle4d t = new AxisAngle4d(vx, vy, vz, a); + Transform3D trans = new Transform3D(); + trans.setRotation(t); + rot.setTransform(trans); + } + + private void rotate(Quat4d quat) { + Transform3D trans = new Transform3D(); + trans.setRotation(quat); + rot.setTransform(trans); + } + + // キャラクターのpart(体の部分)を回転させる + public void rotate(String part, double vx, double vy, double vz, double a) { + getPart(part).rotate(vx, vy, vz, a); + } + + public void apply(Property3D p, boolean enableUndo) { + if (enableUndo) { + undoBuffer.push(p); + } + p.applyTo(this); + } + + // 自分を複製する(クローンを作る) + public Object3D duplicate() { + Object3D obj = new Object3D(this); + return obj; + } + + // v(訪問者)に訪問先を教える + public void accept(ObjectVisitor v) { + int i; + v.preVisit(this); + if (children != null) { + for (i = 0; i < children.length; i++) + children[i].accept(v); + } else + ; + v.postVisit(this); + } + + // objectに子供がいるかどうかを調べる + public boolean hasChildren() { + if (this.children != null && this.children.length > 0) + return true; + else + return false; + } + + /* + * undoするポイントを設定する。 + */ + public void setUndoMark() { + undoBuffer.setUndoMark(); + } + + /* + * undoする。 + */ + public void undo() { + Iterator iterator = undoBuffer.undo().iterator(); + while (iterator.hasNext()) { + Property3D p = iterator.next(); + p.applyTo(this); + } + } + + /** + * 当たり判定処理、Oriented Bounding Box(OBB)をする。 0:最大面積法 1:最長対角線法 + */ + public OBB getOBB(int pattern) { + if (obb == null) { + Node node = getPrimitiveNode(); + if (node == null) return null; + if (node instanceof Box) { + // Boxの場合 + Box box = ((Box)node); + double xDim = box.getXdimension(); + double yDim = box.getYdimension(); + double zDim = box.getZdimension(); + obb = new OBB(new Vector3d(-xDim, -yDim, -zDim), + new Vector3d(-xDim, yDim, -zDim), + new Vector3d(-xDim, -yDim, zDim), + new Vector3d(-xDim, yDim, zDim), + new Vector3d(xDim, -yDim, -zDim), + new Vector3d(xDim, yDim, -zDim), + new Vector3d(xDim, -yDim, zDim), + new Vector3d(xDim, yDim, zDim)); + } else if (node instanceof Cylinder) { + // Cylinderの場合 + Cylinder cylinder = ((Cylinder)node); + double xDim = cylinder.getRadius(); + double yDim = cylinder.getHeight() / 2; + double zDim = cylinder.getRadius(); + obb = new OBB(new Vector3d(-xDim, -yDim, -zDim), + new Vector3d(-xDim, yDim, -zDim), + new Vector3d(-xDim, -yDim, zDim), + new Vector3d(-xDim, yDim, zDim), + new Vector3d(xDim, -yDim, -zDim), + new Vector3d(xDim, yDim, -zDim), + new Vector3d(xDim, -yDim, zDim), + new Vector3d(xDim, yDim, zDim)); + } else if (node instanceof Cone) { + // Coneの場合 + Cone cone = ((Cone)node); + double xDim = cone.getRadius(); + double yDim = cone.getHeight() / 2; + double zDim = cone.getRadius(); + obb = new OBB(new Vector3d(-xDim, -yDim, -zDim), + new Vector3d(-xDim, yDim, -zDim), + new Vector3d(-xDim, -yDim, zDim), + new Vector3d(-xDim, yDim, zDim), + new Vector3d(xDim, -yDim, -zDim), + new Vector3d(xDim, yDim, -zDim), + new Vector3d(xDim, -yDim, zDim), + new Vector3d(xDim, yDim, zDim)); + } else if (node instanceof Sphere) { + // Sphereの場合 + Sphere sphere = ((Sphere)node); + double xDim = sphere.getRadius(); + double yDim = sphere.getRadius(); + double zDim = sphere.getRadius(); + obb = new OBB(new Vector3d(-xDim, -yDim, -zDim), + new Vector3d(-xDim, yDim, -zDim), + new Vector3d(-xDim, -yDim, zDim), + new Vector3d(-xDim, yDim, zDim), + new Vector3d(xDim, -yDim, -zDim), + new Vector3d(xDim, yDim, -zDim), + new Vector3d(xDim, -yDim, zDim), + new Vector3d(xDim, yDim, zDim)); + } else { + if (!(node instanceof Shape3D)) return null; + // Shape3Dの場合 + Shape3D shape = (Shape3D)node; + double coordinate[] = new double[3]; + + // OBBの作成に用いる頂点列の取得 + ArrayList vertex3DList = new ArrayList(); + if (shape.getGeometry() instanceof IndexedGeometryArray) { + // IndexedGeometryArrayの場合 + IndexedGeometryArray iga = (IndexedGeometryArray) shape.getGeometry(); + for (int i = 0; i < iga.getIndexCount() - 3; i++) { + iga.getCoordinates(iga.getCoordinateIndex(i), coordinate); + Vector3d p = new Vector3d(coordinate[0], coordinate[1], coordinate[2]); + vertex3DList.add(p); + } + } else if (shape.getGeometry() instanceof GeometryStripArray) { + // GeometryStripArrayの場合 + GeometryStripArray gsa = (GeometryStripArray) shape.getGeometry(); + for (int i = 0; i < gsa.getVertexCount(); i++) { + gsa.getCoordinates(i, coordinate); + Vector3d p = new Vector3d(coordinate[0], coordinate[1], coordinate[2]); + vertex3DList.add(p); + } + } else if (shape.getGeometry() instanceof TriangleArray) { + // TriangleArrayの場合 + TriangleArray tra = (TriangleArray) shape.getGeometry(); + for (int i = 0; i < tra.getVertexCount(); i++) { + tra.getCoordinates(i, coordinate); + Vector3d p = new Vector3d(coordinate[0], coordinate[1], coordinate[2]); + vertex3DList.add(p); + } + } + + if (pattern == 0) { + // 最大面積法 + Vector3d cv1 = new Vector3d(); + Vector3d cv2 = new Vector3d(); + + double cvMax = 0.0; + Vector3d axis1 = new Vector3d(); // 3D頂点から求めた法線ベクトル + Vector3d axis2 = new Vector3d(); // 2D頂点から求めた法線ベクトル + Vector3d axis3 = new Vector3d(); // axis1,axis2の外積から求めた法線ベクトル + + // 面積を3D頂点リストから求め、法線を求める処理 + for (int i = 0; i < vertex3DList.size(); i += 3) { + cv1.sub(vertex3DList.get(i + 2), vertex3DList.get(i)); + cv2.sub(vertex3DList.get(i + 1), vertex3DList.get(i)); + Vector3d cv = new Vector3d(); + cv.cross(cv1, cv2); + if (i == 0 || cv.length() >= cvMax) { + cvMax = cv.length(); + axis1 = cv; + } + } + + ProjectionResult pr1 = GeometryUtility.projection3D(vertex3DList, axis1); + // 辺のを2D頂点リストから求め、法線を求める処理 + for (int i = 0; i < pr1.vertexList.size() - 1; i++) { + Vector3d cv = new Vector3d(); + cv.sub(vertex3DList.get(i + 1), vertex3DList.get(i)); + if (i == 0 || cv.length() >= cvMax) { + cvMax = cv.length(); + axis2 = cv; + } + } + + + ProjectionResult pr2 = GeometryUtility.projection2D(pr1.vertexList, axis2); + // axis1,axis2で外積でaxis3を求める。 + axis3.cross(axis1, axis2); + + ProjectionResult pr3 = GeometryUtility.projection2D(pr1.vertexList, axis3); + AxisResult ar = new AxisResult(axis1, axis2, axis3); + + // ここから最大面積法で求めた点を取得していく処理 + + // OBBの生成 + obb = new OBB(); + + // No.1 + ar.axis1.scale(pr1.min); + ar.axis2.scale(pr2.min); + ar.axis3.scale(pr3.min); + ar.axis1.add(ar.axis2); + ar.axis3.add(ar.axis1); + obb.addVertex(ar.axis3); + + // No.2 + ar = new AxisResult(axis1, axis2, axis3); + ar.axis1.scale(pr1.min); + ar.axis2.scale(pr2.max); + ar.axis3.scale(pr3.min); + ar.axis1.add(ar.axis2); + ar.axis3.add(ar.axis1); + obb.addVertex(ar.axis3); + + // No.3 + ar = new AxisResult(axis1, axis2, axis3); + ar.axis1.scale(pr1.min); + ar.axis2.scale(pr2.min); + ar.axis3.scale(pr3.max); + ar.axis1.add(ar.axis2); + ar.axis3.add(ar.axis1); + obb.addVertex(ar.axis3); + + // No.4 + ar = new AxisResult(axis1, axis2, axis3); + ar.axis1.scale(pr1.min); + ar.axis2.scale(pr2.max); + ar.axis3.scale(pr3.max); + ar.axis1.add(ar.axis2); + ar.axis3.add(ar.axis1); + obb.addVertex(ar.axis3); + + // No.5 + ar = new AxisResult(axis1, axis2, axis3); + ar.axis1.scale(pr1.max); + ar.axis2.scale(pr2.min); + ar.axis3.scale(pr3.min); + ar.axis1.add(ar.axis2); + ar.axis3.add(ar.axis1); + obb.addVertex(ar.axis3); + + // No.6 + ar = new AxisResult(axis1, axis2, axis3); + ar.axis1.scale(pr1.max); + ar.axis2.scale(pr2.max); + ar.axis3.scale(pr3.min); + ar.axis1.add(ar.axis2); + ar.axis3.add(ar.axis1); + obb.addVertex(ar.axis3); + + // No.7 + ar = new AxisResult(axis1, axis2, axis3); + ar.axis1.scale(pr1.max); + ar.axis2.scale(pr2.min); + ar.axis3.scale(pr3.max); + ar.axis1.add(ar.axis2); + ar.axis3.add(ar.axis1); + obb.addVertex(ar.axis3); + + // No.8 + ar = new AxisResult(axis1, axis2, axis3); + ar.axis1.scale(pr1.max); + ar.axis2.scale(pr2.max); + ar.axis3.scale(pr3.max); + ar.axis1.add(ar.axis2); + ar.axis3.add(ar.axis1); + obb.addVertex(ar.axis3); + + // 面および境界多面体の生成 + obb.createPlanes(); + } else { + // AABB + double minX, maxX, minY, maxY, minZ, maxZ; + minX = maxX = vertex3DList.get(0).x; + minY = maxY = vertex3DList.get(0).y; + minZ = maxZ = vertex3DList.get(0).z; + for (int n = 1; n < vertex3DList.size(); n++) { + Vector3d v = vertex3DList.get(n); + if (minX > v.x) minX = v.x; + if (maxX < v.x) maxX = v.x; + if (minY > v.y) minY = v.y; + if (maxY < v.y) maxY = v.y; + if (minZ > v.z) minZ = v.z; + if (maxZ < v.z) maxZ = v.z; + } + obb = new OBB(new Vector3d(minX, minY, minZ), + new Vector3d(minX, maxY, minZ), + new Vector3d(minX, minY, maxZ), + new Vector3d(minX, maxY, maxZ), + new Vector3d(maxX, minY, minZ), + new Vector3d(maxX, maxY, minZ), + new Vector3d(maxX, minY, maxZ), + new Vector3d(maxX, maxY, maxZ)); + } + } + } + return obb; + } + + private class AxisResult { + Vector3d axis1; + Vector3d axis2; + Vector3d axis3; + + AxisResult(Vector3d a1, Vector3d a2, Vector3d a3) { + axis1 = (Vector3d) a1.clone(); + axis2 = (Vector3d) a2.clone(); + axis3 = (Vector3d) a3.clone(); + } + } + + // Quaternion3D の applyTo() 以外からは呼ばないこと + void setQuaternion(Quaternion3D quaternion) { + this.quaternion = quaternion; + rotate(quaternion.getQuat()); + } + + public Quaternion3D getQuaternion() { + return quaternion; + } + + public boolean isLODSet() { + return bLOD; + } + + public LOD getLOD() { + return lodNode; + } +} diff --git a/src/main/java/framework/model3D/ObjectVisitor.java b/src/main/java/framework/model3D/ObjectVisitor.java new file mode 100644 index 0000000..28ef67c --- /dev/null +++ b/src/main/java/framework/model3D/ObjectVisitor.java @@ -0,0 +1,56 @@ +package framework.model3D; + +import javax.media.j3d.Transform3D; +import java.util.ArrayList; + +/** + * オブジェクトの階層構造をトラバースするビジター + * @author 新田直也 + * + */ +public abstract class ObjectVisitor { + /** + * 根から訪問節点までのパス上に存在する変換行列 + */ + protected ArrayList stackList = new ArrayList(); + + /* + * 節点(オブジェクト)を訪問する直前に呼ばれるメソッド + * @param obj 訪問節点 + */ + public abstract void preVisit(Object3D obj); + /** + * 節点(オブジェクトを訪問した直後に呼ばれるメソッド) + * @param obj 訪問節点 + */ + public abstract void postVisit(Object3D obj); + + /** + * 1階層潜ったときに変換行列を増やす + * @param obj 新しく訪問した節点 + */ + protected void pushTransform(Object3D obj) { + Transform3D transPos = new Transform3D(); + obj.pos.getTransform(transPos); + stackList.add(transPos); + Transform3D transRot = new Transform3D(); + obj.rot.getTransform(transRot); + stackList.add(transRot); + Transform3D transScale = new Transform3D(); + obj.scale.getTransform(transScale); + stackList.add(transScale); + Transform3D transCenter = new Transform3D(); + obj.center.getTransform(transCenter); + stackList.add(transCenter); + } + + /** + * 1階層復帰したときに変換行列を 減らす + */ + protected void popTransform() { + for (int i = 0; i < 4; i++) { + stackList.remove(stackList.size() - 1); + } + } + +} diff --git a/src/main/java/framework/model3D/Placeable.java b/src/main/java/framework/model3D/Placeable.java new file mode 100644 index 0000000..23f3bc7 --- /dev/null +++ b/src/main/java/framework/model3D/Placeable.java @@ -0,0 +1,13 @@ +package framework.model3D; + +import javax.media.j3d.TransformGroup; + +/** + * 配置できるもの全て + * @author 新田直也 + * + */ +public interface Placeable { + abstract TransformGroup getTransformGroupToPlace(); + abstract BaseObject3D getBody(); +} diff --git a/src/main/java/framework/model3D/Position3D.java b/src/main/java/framework/model3D/Position3D.java new file mode 100644 index 0000000..0fef55c --- /dev/null +++ b/src/main/java/framework/model3D/Position3D.java @@ -0,0 +1,151 @@ +package framework.model3D; + +import javax.vecmath.Vector3d; + +/** + * 座標値を表す + * @author 新田直也 + * + */ +public class Position3D extends Property3D { + private double x; + private double y; + private double z; + + // コンストラクタ + public Position3D() { + x = 0.0; + y = 0.0; + z = 0.0; + } + + // コンストラクタ + public Position3D(double px, double py, double pz) { + x = px; + y = py; + z = pz; + } + + public Position3D(Vector3d v) { + x = v.x; + y = v.y; + z = v.z; + } + + // property3Dの抽象クラスを埋める + public void applyTo(Object3D o) { + o.setPosition(this); + } + + // コピーコンストラクタ + public Position3D(Position3D p) { + this.x = p.x; + this.y = p.y; + this.z = p.z; + } + + public Position3D set(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + // 加算 + public Position3D add(double x, double y, double z) { + this.x += x; + this.y += y; + this.z += z; + return this; + } + + // 加算 + public Position3D add(Position3D p) { + this.x += p.x; + this.y += p.y; + this.z += p.z; + return this; + } + + // 加算 + public Position3D add(Vector3d v) { + this.x += v.x; + this.y += v.y; + this.z += v.z; + return this; + } + + // 減算 + public Position3D sub(double x, double y, double z) { + this.x -= x; + this.y -= y; + this.z -= z; + return this; + } + + // 減算 + public Position3D sub(Position3D p) { + this.x -= p.x; + this.y -= p.y; + this.z -= p.z; + return this; + } + + // 減算 + public Position3D sub(Vector3d v) { + this.x -= v.x; + this.y -= v.y; + this.z -= v.z; + return this; + } + + // スカラー倍 + public Position3D mul(double d) { + this.x *= d; + this.y *= d; + this.z *= d; + return this; + } + + public Position3D setX(double x) { + this.x = x; + return this; + } + + public double getX() { + return x; + } + + public Position3D setY(double y) { + this.y = y; + return this; + } + + public double getY() { + return y; + } + + public Position3D setZ(double z) { + this.z = z; + return this; + } + + public double getZ() { + return z; + } + + public Vector3d getVector3d() { + return new Vector3d(x, y, z); + } + + public Position3D setVector3d(Vector3d v) { + x = v.x; + y = v.y; + z = v.z; + return this; + } + + public Position3D clone() { + return new Position3D(this); + } +} diff --git a/src/main/java/framework/model3D/ProjectionResult.java b/src/main/java/framework/model3D/ProjectionResult.java new file mode 100644 index 0000000..a5ae6d9 --- /dev/null +++ b/src/main/java/framework/model3D/ProjectionResult.java @@ -0,0 +1,11 @@ +package framework.model3D; + +import javax.vecmath.Vector3d; +import java.util.ArrayList; + + +public class ProjectionResult { + double max = 0.0; + double min = 0.0; + ArrayList vertexList = new ArrayList(); +} diff --git a/src/main/java/framework/model3D/Property3D.java b/src/main/java/framework/model3D/Property3D.java new file mode 100644 index 0000000..9be574e --- /dev/null +++ b/src/main/java/framework/model3D/Property3D.java @@ -0,0 +1,8 @@ +package framework.model3D; + + +public abstract class Property3D{ + public abstract void applyTo(Object3D o); + + public abstract Property3D clone(); +} diff --git a/src/main/java/framework/model3D/Quaternion3D.java b/src/main/java/framework/model3D/Quaternion3D.java new file mode 100644 index 0000000..1d048a3 --- /dev/null +++ b/src/main/java/framework/model3D/Quaternion3D.java @@ -0,0 +1,103 @@ +package framework.model3D; + +import javax.vecmath.AxisAngle4d; +import javax.vecmath.Quat4d; + +public class Quaternion3D extends Property3D{ + private Quat4d quaternion; + + //コンストラクタ============================= + public Quaternion3D(){ + AxisAngle4d aa = new AxisAngle4d(0.0, 0.0, 1.0, 0.0); + quaternion = new Quat4d(); + quaternion.set(aa); + } + + public Quaternion3D(AxisAngle4d aa){ + quaternion = new Quat4d(); + quaternion.set(aa); + } + + public Quaternion3D(double x,double y,double z,double w){ + AxisAngle4d aa = new AxisAngle4d(x, y, z, w); + quaternion = new Quat4d(); + quaternion.set(aa); + } + public Quaternion3D(Quaternion3D q){ + this.quaternion = (Quat4d)q.getQuat().clone(); + } + public Quat4d getQuat(){ + return quaternion; + } + + //ゲッタ================================= + public double getX(){ + return quaternion.x; + } + public double getY(){ + return quaternion.y; + } + public double getZ(){ + return quaternion.z; + } + public double getW(){ + return quaternion.w; + } + public Quaternion3D getInterpolate(Quaternion3D q, double alpha){ + quaternion.interpolate(q.getQuat(), alpha); + return this; + } + + public AxisAngle4d getAxisAngle() { + AxisAngle4d axisAngle = new AxisAngle4d(); + axisAngle.set(quaternion); + return axisAngle; + } + + //セッタ================================= + public Quaternion3D setQuaternion(double x, double y, double z, double w){ + quaternion = new Quat4d(x, y, z, w); + return this; + } + + public Quaternion3D setAxisAngle(double x, double y, double z, double w){ + AxisAngle4d aa = new AxisAngle4d(x, y, z, w); + quaternion = new Quat4d(); + quaternion.set(aa); + return this; + } + + public Quaternion3D add(AxisAngle4d aa){ + Quat4d q = new Quat4d(); + q.set(aa); + q.mul(quaternion); + quaternion = q; + return this; + } + + public Quaternion3D mul(Quaternion3D q){ + quaternion.mul(q.getQuat()); + return this; + } + + @Override + public void applyTo(Object3D o) { + // TODO Auto-generated method stub + o.setQuaternion(this); + } + + @Override + public Property3D clone() { + // TODO Auto-generated method stub + return new Quaternion3D(this); + } + + public double norm() { + return quaternion.w * quaternion.w + quaternion.x * quaternion.x + quaternion.y * quaternion.y + quaternion.z * quaternion.z; + } + + public Quaternion3D normalize() { + quaternion.normalize(); + return this; + } +} diff --git a/src/main/java/framework/model3D/ReflectionMapGenerator.java b/src/main/java/framework/model3D/ReflectionMapGenerator.java new file mode 100644 index 0000000..43e6552 --- /dev/null +++ b/src/main/java/framework/model3D/ReflectionMapGenerator.java @@ -0,0 +1,15 @@ +package framework.model3D; + +import javax.vecmath.Color4f; + +public class ReflectionMapGenerator extends MapGenerator { + private Color4f blendColor = null; + + public ReflectionMapGenerator(Color4f blendColor) { + this.blendColor = blendColor; + } + + public Color4f getBlendColor() { + return blendColor; + } +} diff --git a/src/main/java/framework/model3D/ShadowVolume.java b/src/main/java/framework/model3D/ShadowVolume.java new file mode 100644 index 0000000..1e8125d --- /dev/null +++ b/src/main/java/framework/model3D/ShadowVolume.java @@ -0,0 +1,577 @@ +package framework.model3D; + +import com.sun.j3d.utils.geometry.Box; +import com.sun.j3d.utils.geometry.Cone; +import com.sun.j3d.utils.geometry.Cylinder; +import com.sun.j3d.utils.geometry.Sphere; + +import javax.media.j3d.*; +import javax.vecmath.Point3d; +import javax.vecmath.Point3f; +import javax.vecmath.Vector3d; +import javax.vecmath.Vector3f; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Hashtable; +import java.util.Iterator; + +/** + * ステンシルバッファを用いたシャドウボリューム + * @author 新田直也 + * + */ +public class ShadowVolume { + private static final boolean WIRE_FRAME_VIEW = false; + + private Light light = null; + private double shadowDepth = 10.0; + + private IndexedQuadArray volumeGeometry = null; + private Shape3D shadowVolumeFront = null; + private Shape3D shadowVolumeBack = null; + + private int vertexCount = 0; + private double coordinates[] = null; + private int indicies[] = null; + private Hashtable edges = null; + private ArrayList triangles = null; + + /** + * 平行光源用のシャドウボリューム + * @param occuluder 遮蔽オブジェクト(影を落とすオブジェクト) + * @param light 光源 + * @param shadowDepth 影の深さ + * @param localToWorld 遮蔽オブジェクト用の座標変換 + */ + public ShadowVolume(BaseObject3D occuluder, Light light, double shadowDepth, + Transform3D localToWorld) { + this.light = light; + this.shadowDepth = shadowDepth; + + // 幾何情報の抽出 + if (!extractGeometricInformation(occuluder)) return; + + // シャドウボリュームの表面を生成 + volumeGeometry = createVolumeSurface(light, shadowDepth, localToWorld, coordinates, edges, triangles); + + // シャドウボリュームオブジェクトの生成 + createShadowVolumeObject(volumeGeometry, occuluder); + } + + /** + * シャドウボリュームの更新 + * @param localToWorld 遮蔽オブジェクト用の座標変換 + */ + public void update(Transform3D localToWorld) { + // シャドウボリュームの表面を再生成 + volumeGeometry = createVolumeSurface(light, shadowDepth, localToWorld, coordinates, edges, triangles); + + // シャドウボリュームオブジェクトの更新 + shadowVolumeFront.removeAllGeometries(); + shadowVolumeFront.addGeometry(volumeGeometry); + shadowVolumeBack.removeAllGeometries(); + shadowVolumeBack.addGeometry(volumeGeometry); + } + + /** + * 幾何情報の抽出 + * @param shadowCastingObject 抽出元のオブジェクト + * @return true --- 正常に抽出できた, false --- 正常に抽出できなかった + */ + private boolean extractGeometricInformation(BaseObject3D shadowCastingObject) { + Node node = shadowCastingObject.getPrimitiveNode(); + if (node == null) return false; + // GeometryArrayの習得 + ArrayList geometries = new ArrayList(); + Geometry g = null; + if (node instanceof Shape3D) { + g = ((Shape3D)node).getGeometry(); + geometries.add(g); + } else if (node instanceof Cone) { + g = ((Cone)node).getShape(Cone.BODY).getGeometry(); + geometries.add(g); + g = ((Cone)node).getShape(Cone.CAP).getGeometry(); + geometries.add(g); + } else if (node instanceof Cylinder) { + g = ((Cylinder)node).getShape(Cylinder.BODY).getGeometry(); + geometries.add(g); + g = ((Cylinder)node).getShape(Cylinder.TOP).getGeometry(); + geometries.add(g); + g = ((Cylinder)node).getShape(Cylinder.BOTTOM).getGeometry(); + geometries.add(g); + } else if (node instanceof Box) { + g = ((Box)node).getShape(Box.TOP).getGeometry(); + geometries.add(g); + g = ((Box)node).getShape(Box.BOTTOM).getGeometry(); + geometries.add(g); + g = ((Box)node).getShape(Box.FRONT).getGeometry(); + geometries.add(g); + g = ((Box)node).getShape(Box.BACK).getGeometry(); + geometries.add(g); + g = ((Box)node).getShape(Box.LEFT).getGeometry(); + geometries.add(g); + g = ((Box)node).getShape(Box.RIGHT).getGeometry(); + geometries.add(g); + } else if (node instanceof Sphere) { + g = ((Sphere)node).getShape().getGeometry(); + geometries.add(g); + } else { + return false; + } + + // 頂点の座標値を取得し、設定 + vertexCount = 0; + for (int n = 0; n < geometries.size(); n++) { + g = geometries.get(n); + if (g instanceof GeometryArray) { + // GeometryArray の場合 + GeometryArray geometryArray = (GeometryArray)g; + vertexCount += geometryArray.getVertexCount(); + } + } + coordinates = new double[vertexCount * 3 * 2]; + indicies = new int[vertexCount * 4 * 3]; // 辺の数を頂点の数の最大3倍と考えている + int base = 0; + for (int n = 0; n < geometries.size(); n++) { + g = geometries.get(n); + if (g instanceof GeometryArray) { + // GeometryArray の場合 + GeometryArray geometryArray = (GeometryArray)g; + double coordinate[] = new double[3]; + int count = geometryArray.getVertexCount(); + for (int v = 0; v < count; v++) { + geometryArray.getCoordinate(v, coordinate); + coordinates[(v + base) * 3] = coordinate[0]; + coordinates[(v + base) * 3 + 1] = coordinate[1]; + coordinates[(v + base) * 3 + 2] = coordinate[2]; + } + base += count; + } + } + + // 面と辺の情報を作成 + edges = new Hashtable(); + triangles = new ArrayList(); + base = 0; + for (int n = 0; n < geometries.size(); n++) { + g = geometries.get(n); + if (g instanceof GeometryArray) { + // GeometryArray の場合 + GeometryArray geometryArray = (GeometryArray)g; + Point3d p1 = new Point3d(); + Point3d p2 = new Point3d(); + Point3d p3 = new Point3d(); + int v1, v2, v3; + if (g instanceof TriangleStripArray) { + // TriangleStripArray の場合 + TriangleStripArray triStripArray = (TriangleStripArray)g; + int num = triStripArray.getNumStrips(); + int strips[] = new int[num]; + triStripArray.getStripVertexCounts(strips); + int index = base; + for (int j = 0; j < num; j++) { + index += 2; + for (int i = 2; i < strips[j]; i += 2) { + addTriangle(index - 2, index - 1, index, p1, p2, p3); + if (i <= strips[j] - 2) { + addTriangle(index - 1, index + 1, index, p1, p2, p3); + index += 1; + } + index += 1; + } + } + } else if (g instanceof TriangleFanArray) { + // TriangleFanArray の場合 + TriangleFanArray triFanArray = (TriangleFanArray)g; + int num = triFanArray.getNumStrips(); + int strips[] = new int[num]; + triFanArray.getStripVertexCounts(strips); + int index = base; + for (int j = 0; j < num; j++) { + v1 = index; + index += 1; + for (int i = 1; i < strips[j] - 1; i += 1) { + addTriangle(v1, index, index + 1, p1, p2, p3); + index += 1; + } + } + } else if (g instanceof IndexedTriangleArray) { + // IndexedTriangleArray の場合 + IndexedTriangleArray triArray = (IndexedTriangleArray)g; + int indexCount = triArray.getIndexCount(); + for (int i = 0; i < indexCount; i += 3) { + v1 = triArray.getCoordinateIndex(i) + base; + v2 = triArray.getCoordinateIndex(i + 1) + base; + v3 = triArray.getCoordinateIndex(i + 2) + base; + addTriangle(v1, v2, v3, p1, p2, p3); + } + } else if (g instanceof IndexedTriangleStripArray) { + // IndexedTriangleStripArray の場合 + IndexedTriangleStripArray triStripArray = (IndexedTriangleStripArray)g; + int num = triStripArray.getNumStrips(); + int strips[] = new int[num]; + triStripArray.getStripIndexCounts(strips); + int index = 0; + for (int j = 0; j < num; j++) { + index += 2; + for (int i = 2; i < strips[j]; i += 2) { + v1 = triStripArray.getCoordinateIndex(index - 2) + base; + v2 = triStripArray.getCoordinateIndex(index - 1) + base; + v3 = triStripArray.getCoordinateIndex(index) + base; + addTriangle(v1,v2, v3, p1, p2, p3); + if (i <= strips[j] - 2) { + v1 = v2; + v2 = triStripArray.getCoordinateIndex(index + 1) + base; + addTriangle(v1, v2, v3, p1, p2, p3); + index += 1; + } + index += 1; + } + } + } else if (g instanceof IndexedTriangleFanArray) { + // IndexedTriangleFanArray の場合 + IndexedTriangleFanArray triFanArray = (IndexedTriangleFanArray)g; + int num = triFanArray.getNumStrips(); + int strips[] = new int[num]; + triFanArray.getStripIndexCounts(strips); + int index = 0; + for (int j = 0; j < num; j++) { + v1 = triFanArray.getCoordinateIndex(index) + base; + index += 1; + for (int i = 1; i < strips[j] - 1; i += 1) { + v2 = triFanArray.getCoordinateIndex(index) + base; + v3 = triFanArray.getCoordinateIndex(index + 1) + base; + addTriangle(v1, v2, v3, p1, p2, p3); + index += 1; + } + } + } + base += geometryArray.getVertexCount(); + } + } + return true; + } + + private void addTriangle(int v1, int v2, int v3, Point3d p1, Point3d p2, Point3d p3) { + Vector3d vec1; + Vector3d vec2; + Vector3d vec; + Triangle tri; + Edge e; + p1.x = coordinates[v1 * 3]; + p1.y = coordinates[v1 * 3 + 1]; + p1.z = coordinates[v1 * 3 + 2]; + p2.x = coordinates[v2 * 3]; + p2.y = coordinates[v2 * 3 + 1]; + p2.z = coordinates[v2 * 3 + 2]; + p3.x = coordinates[v3 * 3]; + p3.y = coordinates[v3 * 3 + 1]; + p3.z = coordinates[v3 * 3 + 2]; + vec = new Vector3d(p1); + vec1 = new Vector3d(p2); + vec2 = new Vector3d(p3); + vec1.sub(vec); + vec2.sub(vec); + vec1.cross(vec1, vec2); + if (vec1.length() < GeometryUtility.TOLERANCE) { + return; + } + vec1.normalize(); + tri = new Triangle(v1, v2, v3, vec1); + triangles.add(tri); + e = edges.get(new Integer(v2 * vertexCount + v1)); // 逆向きに回っている辺のみ共有 + if (e != null) { + tri.edges[0] = e; + e.triangles[1] = tri; + } else { + vec = new Vector3d(p2); + vec.sub(p1); + e = new Edge(v1, v2, vec); + tri.edges[0] = e; + e.triangles[0] = tri; + edges.put(new Integer(v1 * vertexCount + v2), e); + } + e = edges.get(new Integer(v3 * vertexCount + v2)); // 逆向きに回っている辺のみ共有 + if (e != null) { + tri.edges[1] = e; + e.triangles[1] = tri; + } else { + vec = new Vector3d(p3); + vec.sub(p2); + e = new Edge(v2, v3, vec); + tri.edges[1] = e; + e.triangles[0] = tri; + edges.put(new Integer(v2 * vertexCount + v3), e); + } + e = edges.get(new Integer(v1 * vertexCount + v3)); // 逆向きに回っている辺のみ共有 + if (e != null) { + tri.edges[2] = e; + e.triangles[1] = tri; + } else { + vec = new Vector3d(p1); + vec.sub(p3); + e = new Edge(v3, v1, vec); + tri.edges[2] = e; + e.triangles[0] = tri; + edges.put(new Integer(v3 * vertexCount + v1), e); + } + } + + /** + * シャドウボリュームのジオメトリの作成 + * @param light + * @param shadowDepth + * @param localToWorld + * @param coordinates + * @param edges + * @param triangles + * @return シャドウボリュームのジオメトリ + */ + private IndexedQuadArray createVolumeSurface(Light light, double shadowDepth, Transform3D localToWorld, + double[] coordinates, Hashtable edges, ArrayList triangles) { + // オブジェクトに対する相対座標系に変換 + Vector3d displacement = null; + Point3d localLightPosition = null; + Transform3D worldToLocal; + Point3d worldLightPosition = null; + if (light instanceof DirectionalLight) { + // 平行光源の場合 + Vector3f lightDirectionF = new Vector3f(); + ((DirectionalLight)light).getDirection(lightDirectionF); + displacement = new Vector3d(lightDirectionF); + displacement.scale(shadowDepth); + worldToLocal = new Transform3D(localToWorld); + worldToLocal.invert(); + worldToLocal.transform(displacement); + } else if (light instanceof PointLight) { + // 点光源の場合 + Point3f worldLightPositionF = new Point3f(); + ((PointLight)light).getPosition(worldLightPositionF); + worldLightPosition = new Point3d(worldLightPositionF); + localLightPosition = new Point3d(worldLightPosition); + worldToLocal = new Transform3D(localToWorld); + worldToLocal.invert(); + worldToLocal.transform(localLightPosition); + } else { + return null; + } + + Triangle tri; + // すべての面の光線に対する向きを計算 + if (displacement != null) { + for (int n = 0; n < triangles.size(); n++) { + tri = triangles.get(n); + tri.innerProduct = tri.normal.dot(displacement); + } + } else if (localLightPosition != null) { + for (int n = 0; n < triangles.size(); n++) { + tri = triangles.get(n); + Vector3d v = new Vector3d(coordinates[tri.v1*3], coordinates[tri.v1*3 + 1], coordinates[tri.v1*3 + 2]); + v.sub(localLightPosition); + tri.innerProduct = tri.normal.dot(v); + } + } + Collection es = edges.values(); + Edge edge; + int index = 0; + boolean doesGenerateSurface = false; + double i0 = 0, i1 = 0; + Vector3d displacement1 = displacement; + Vector3d displacement2 = displacement; + Vector3d tempVec = new Vector3d(); + Point3d worldPos = new Point3d(); + if (localLightPosition != null) { + // 点光源の場合 + displacement1 = new Vector3d(); + displacement2 = new Vector3d(); + } + for (Iterator it = es.iterator(); it.hasNext(); ) { + edge = it.next(); + // edgeに対してシャドウボリュームの表面を生成するか否かの判定 + doesGenerateSurface = false; + if (edge.triangles[1] == null) { + // 開多面体の場合 + i0 = edge.triangles[0].innerProduct; + if (i0 < -GeometryUtility.TOLERANCE) { + // 光源に対して表向きの面の辺に対してのみ生成 + doesGenerateSurface = true; // 生成する + } + } else { + i0 = edge.triangles[0].innerProduct; + i1 = edge.triangles[1].innerProduct; + if (i0 * i1 < GeometryUtility.TOLERANCE) { + if (i0 * i1 > -GeometryUtility.TOLERANCE) { + // 辺を挟む2つの面の一方が光線の向きに対して平行である + if (i0 < -GeometryUtility.TOLERANCE || i1 < -GeometryUtility.TOLERANCE) { + // いずれかの面が表向きだった場合 + tempVec.cross(edge.triangles[0].normal, edge.triangles[1].normal); + if (edge.v1ToV2.dot(tempVec) > GeometryUtility.TOLERANCE) { + // 辺が凸の場合のみ生成 + doesGenerateSurface = true; // 生成する + } + } + } else { + // 辺を挟む2つの面が光線の向きに対して裏表である + doesGenerateSurface = true; // 生成する + } + } + } + if (doesGenerateSurface) { + // シャドウボリュームの表面を生成する + if (localLightPosition != null) { + // 点光源の場合 + displacement1.x = coordinates[edge.v1 * 3] - localLightPosition.x; + displacement1.y = coordinates[edge.v1 * 3 + 1] - localLightPosition.y; + displacement1.z = coordinates[edge.v1 * 3 + 2] - localLightPosition.z; + worldPos.x = coordinates[edge.v1 * 3]; + worldPos.y = coordinates[edge.v1 * 3 + 1]; + worldPos.z = coordinates[edge.v1 * 3 + 2]; + localToWorld.transform(worldPos); + displacement1.scale(shadowDepth / worldPos.distance(worldLightPosition)); + displacement2.x = coordinates[edge.v2 * 3] - localLightPosition.x; + displacement2.y = coordinates[edge.v2 * 3 + 1] - localLightPosition.y; + displacement2.z = coordinates[edge.v2 * 3 + 2] - localLightPosition.z; + worldPos.x = coordinates[edge.v2 * 3]; + worldPos.y = coordinates[edge.v2 * 3 + 1]; + worldPos.z = coordinates[edge.v2 * 3 + 2]; + localToWorld.transform(worldPos); + displacement2.scale(shadowDepth / worldPos.distance(worldLightPosition)); + } + if (i0 < -GeometryUtility.TOLERANCE) { + // 表を向いている面はedge.triangles[0]、edgeの向きは常にedge.triangles[0]の辺の向きと一致するので、 + // edgeの向きに従ってシャドウボリュームの表面を生成 + indicies[index] = edge.v2; + indicies[index + 1] = edge.v1; + indicies[index + 2] = edge.v1 + vertexCount; + indicies[index + 3] = edge.v2 + vertexCount; + } else { + // 表を向いている面はedge.triangles[1]、edgeの向きは常にedge.triangles[1]の辺の向きと反対なので、 + // edgeの向きと逆向きにシャドウボリュームの表面を生成 + indicies[index] = edge.v1; + indicies[index + 1] = edge.v2; + indicies[index + 2] = edge.v2 + vertexCount; + indicies[index + 3] = edge.v1 + vertexCount; + } + coordinates[(edge.v1 + vertexCount) * 3] = coordinates[edge.v1 * 3] + displacement1.x; + coordinates[(edge.v1 + vertexCount) * 3 + 1] = coordinates[edge.v1 * 3 + 1] + displacement1.y; + coordinates[(edge.v1 + vertexCount) * 3 + 2] = coordinates[edge.v1 * 3 + 2] + displacement1.z; + coordinates[(edge.v2 + vertexCount) * 3] = coordinates[edge.v2 * 3] + displacement2.x; + coordinates[(edge.v2 + vertexCount) * 3 + 1] = coordinates[edge.v2 * 3 + 1] + displacement2.y; + coordinates[(edge.v2 + vertexCount) * 3 + 2] = coordinates[edge.v2 * 3 + 2] + displacement2.z; + index += 4; + } + } + if (index == 0) return null; + int validIndicies[] = new int[index]; + System.arraycopy(indicies, 0, validIndicies, 0, index); + volumeGeometry = new IndexedQuadArray(coordinates.length / 3, + IndexedQuadArray.COORDINATES | IndexedQuadArray.BY_REFERENCE, + index); + volumeGeometry.setCapability(IndexedQuadArray.ALLOW_COORDINATE_READ); + volumeGeometry.setCapability(IndexedQuadArray.ALLOW_COORDINATE_WRITE); + volumeGeometry.setCapability(IndexedQuadArray.ALLOW_COORDINATE_INDEX_READ); + volumeGeometry.setCapability(IndexedQuadArray.ALLOW_COORDINATE_INDEX_WRITE); + + volumeGeometry.setCoordRefDouble(coordinates); + volumeGeometry.setCoordinateIndices(0, validIndicies); + return volumeGeometry; + } + + /** + * シャドウボリュームの生成 + * @param volumeGeometry + * @param occuluder + */ + private void createShadowVolumeObject(IndexedQuadArray volumeGeometry, BaseObject3D occuluder) { + // シャドウボリュームの表面のオブジェクトの生成 + Appearance frontAp = new Appearance(); + if (WIRE_FRAME_VIEW) frontAp.setMaterial(new Material()); + RenderingAttributes frontRA = new RenderingAttributes(); + if (!WIRE_FRAME_VIEW) frontRA.setStencilEnable(true); // ステンシルバッファを用いる(ただし、Canvas3Dで適切な設定がされている必要がある) + if (!WIRE_FRAME_VIEW) frontRA.setDepthBufferWriteEnable(false); // Zバッファを更新しない + frontRA.setDepthTestFunction(RenderingAttributes.LESS_OR_EQUAL); // Zバッファの影響を受ける + frontRA.setStencilFunction(RenderingAttributes.ALWAYS, 0, ~0); // ステンシルバッファの影響を受けない + frontRA.setStencilOp(RenderingAttributes.STENCIL_KEEP, + RenderingAttributes.STENCIL_KEEP, + RenderingAttributes.STENCIL_INCR); // 見えているピクセルのみステンシルバッファの値を増やす + frontAp.setRenderingAttributes(frontRA); + PolygonAttributes pa1 = new PolygonAttributes(); + if (WIRE_FRAME_VIEW) pa1.setPolygonMode(PolygonAttributes.POLYGON_LINE); + pa1.setCullFace(PolygonAttributes.CULL_BACK); // 裏面を処理しない + frontAp.setPolygonAttributes(pa1); + if (!WIRE_FRAME_VIEW) { + TransparencyAttributes frontTA = new TransparencyAttributes(); // setVisible(false)だとステンシルバッファが更新されないので、透明にする + frontTA.setTransparencyMode(TransparencyAttributes.BLENDED); + frontTA.setTransparency(1.0f); + frontAp.setTransparencyAttributes(frontTA); + } + shadowVolumeFront = new Shape3D(volumeGeometry, frontAp); + shadowVolumeFront.setCapability(Shape3D.ALLOW_GEOMETRY_READ); + shadowVolumeFront.setCapability(Shape3D.ALLOW_GEOMETRY_WRITE); + occuluder.center.addChild(shadowVolumeFront); + + // シャドウボリュームの裏面のオブジェクトの生成 + Appearance backAp= new Appearance(); + if (WIRE_FRAME_VIEW) backAp.setMaterial(new Material()); + RenderingAttributes backRA= new RenderingAttributes(); + if (!WIRE_FRAME_VIEW) backRA.setStencilEnable(true); // ステンシルバッファを用いる(ただし、Canvas3Dで適切な設定がされている必要がある) + if (!WIRE_FRAME_VIEW) backRA.setDepthBufferWriteEnable(false); // Zバッファを更新しない + backRA.setDepthTestFunction(RenderingAttributes.LESS_OR_EQUAL); // Zバッファの影響を受ける + backRA.setStencilFunction(RenderingAttributes.ALWAYS, 0, ~0); // ステンシルバッファの影響を受けない + backRA.setStencilOp(RenderingAttributes.STENCIL_KEEP, + RenderingAttributes.STENCIL_KEEP, + RenderingAttributes.STENCIL_DECR); // 見えているピクセルのみステンシルバッファの値を減らす + backAp.setRenderingAttributes(backRA); + PolygonAttributes pa2 = new PolygonAttributes(); + if (WIRE_FRAME_VIEW) pa2.setPolygonMode(PolygonAttributes.POLYGON_LINE); + pa2.setCullFace(PolygonAttributes.CULL_FRONT); // 表面を処理しない + backAp.setPolygonAttributes(pa2); + if (!WIRE_FRAME_VIEW) { + TransparencyAttributes backTA = new TransparencyAttributes(); // setVisible(false)だとステンシルバッファが更新されないので、透明にする + backTA.setTransparencyMode(TransparencyAttributes.BLENDED); + backTA.setTransparency(1.0f); + backAp.setTransparencyAttributes(backTA); + } + shadowVolumeBack = new Shape3D(volumeGeometry, backAp); + shadowVolumeBack.setCapability(Shape3D.ALLOW_GEOMETRY_READ); + shadowVolumeBack.setCapability(Shape3D.ALLOW_GEOMETRY_WRITE); + occuluder.center.addChild(shadowVolumeBack); + } + + private class Triangle { + Edge edges[] = new Edge[3]; + Vector3d normal; + double innerProduct; + int v1, v2, v3; + + Triangle(int v1, int v2, int v3, Vector3d normal) { + this.v1 = v1; + this.v2 = v2; + this.v3 = v3; + this.normal = normal; + } + } + + private class Edge { + Triangle triangles[] = new Triangle[2]; + int v1, v2; + Vector3d v1ToV2; +// Vector3d normal = null; + + Edge(int v1, int v2, Vector3d v1ToV2) { + this.v1 = v1; + this.v2 = v2; + this.v1ToV2 = v1ToV2; + } + +// Vector3d normal() { +// if (normal != null) return normal; +// normal = new Vector3d(triangles[0].normal); +// if (triangles[1] != null) { +// normal.add(triangles[1].normal); +// normal.scale(0.5); +// } +// return normal; +// } + } +} diff --git a/src/main/java/framework/model3D/ShadowVolumeVisitor.java b/src/main/java/framework/model3D/ShadowVolumeVisitor.java new file mode 100644 index 0000000..d151507 --- /dev/null +++ b/src/main/java/framework/model3D/ShadowVolumeVisitor.java @@ -0,0 +1,68 @@ +package framework.model3D; + +import javax.media.j3d.Light; +import javax.media.j3d.Transform3D; +import java.util.ArrayList; + +/** + * シャドウボリューム用のビジター + * @author Nitta + * + */ +public class ShadowVolumeVisitor extends ObjectVisitor { + private boolean bInitialize = true; + private ArrayList lights = null; + private double shadowDepth = 0; + + /** + * シャドウボリュームを更新する場合 + */ + public ShadowVolumeVisitor() { + this.bInitialize = false; + Transform3D t = new Transform3D(); + stackList.add(t); + } + + /** + * シャドウボリュームを作成する場合 + * @param lights 光源 + * @param shadowDepth 影の深さ + */ + public ShadowVolumeVisitor(ArrayList lights, double shadowDepth) { + this.lights = lights; + this.shadowDepth = shadowDepth; + this.bInitialize = true; + Transform3D t = new Transform3D(); + stackList.add(t); + } + + @Override + public void preVisit(Object3D obj) { + Transform3D t = new Transform3D(stackList.get(stackList.size() - 1)); + Transform3D t2 = new Transform3D(); + obj.pos.getTransform(t2); + t.mul(t2); + obj.rot.getTransform(t2); + t.mul(t2); + obj.scale.getTransform(t2); + t.mul(t2); + obj.center.getTransform(t2); + t.mul(t2); + stackList.add(t); + } + + @Override + public void postVisit(Object3D obj) { + Transform3D t = stackList.remove(stackList.size() - 1); + if (!obj.hasChildren()) { + // 葉オブジェクトの場合、シャドウボリュームの処理をする + if (bInitialize) { + // 初期化の場合、シャドウボリュームの生成 + obj.createShadowVolume(lights, shadowDepth, t); + } else { + // 初期化でない場合、シャドウボリュームの更新 + obj.updateShadowVolume(t); + } + } + } +} diff --git a/src/main/java/framework/model3D/Terrain3D.java b/src/main/java/framework/model3D/Terrain3D.java new file mode 100644 index 0000000..eac9bb4 --- /dev/null +++ b/src/main/java/framework/model3D/Terrain3D.java @@ -0,0 +1,465 @@ +package framework.model3D; + +import javax.media.j3d.Appearance; +import javax.media.j3d.BoundingBox; +import javax.media.j3d.Shape3D; +import javax.media.j3d.TriangleStripArray; +import javax.vecmath.Point3d; +import javax.vecmath.Vector3d; +import javax.vecmath.Vector3f; + +/** + * メッシュで構成された地形用のオブジェクト(衝突判定が高速化されている) + * @author 新田直也 + * + */ +public class Terrain3D extends BaseObject3D { + static final int MESH_SIZE = 4; + private Position3D origin; + private double sizeX = 0; + private double sizeZ = 0; + private double heightMap[][] = null; + private int meshX = 0; + private int meshZ = 0; + private TriangleStripArray triStripAttay = null; + + public Terrain3D(Position3D origin, double sizeX, double sizeZ, double heightMap[][], Appearance a) { + super(); + this.origin = origin; + this.sizeX = sizeX; + this.sizeZ = sizeZ; + this.heightMap = heightMap; + meshX = heightMap[0].length; + meshZ = heightMap.length; + + int stripVertexCounts[] = new int[meshZ - 1]; + for (int n = 0; n < meshZ - 1; n++) { + stripVertexCounts[n] = meshX * 2; + } + triStripAttay = new TriangleStripArray(meshX * 2 * (meshZ - 1), + TriangleStripArray.COORDINATES | TriangleStripArray.NORMALS, + stripVertexCounts); + int index = 0; + for (int z = 0; z < meshZ; z++) { + for (int x = 0; x < meshX; x++) { + // メッシュごとに三角形を2つづつ生成 + if (z < meshZ - 1) { + // 頂点座標の設定 + // 頂点の法線ベクトルの設定 + triStripAttay.setCoordinate(index, + new Point3d(origin.getX() + sizeX * (double)x, + origin.getY() + heightMap[z][x], + origin.getZ() + sizeZ * (double)z)); + Vector3f normal = calcNormal(z, x); + normal.normalize(); + triStripAttay.setNormal(index, normal); + + triStripAttay.setCoordinate(index + 1, + new Point3d(origin.getX() + sizeX * (double)x, + origin.getY() + heightMap[z + 1][x], + origin.getZ() + sizeZ * (double)(z + 1))); + normal = calcNormal(z + 1, x); + normal.normalize(); + triStripAttay.setNormal(index + 1, normal); + + index += 2; + } + } + } + Appearance a2 = (Appearance)a.cloneNodeComponent(true); + a2.setCapability(Appearance.ALLOW_MATERIAL_READ); + a2.setCapability(Appearance.ALLOW_TEXTURE_READ); + a2.setCapability(Appearance.ALLOW_TEXTURE_WRITE); + a2.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_READ); + a2.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_WRITE); + Shape3D shape = new Shape3D(triStripAttay, a2); + shape.setCapability(Shape3D.ALLOW_APPEARANCE_READ); + shape.setCapability(Shape3D.ALLOW_APPEARANCE_WRITE); + center.addChild(shape); + } + + private Vector3f calcNormal(int z, int x) { + Vector3f normal = new Vector3f(); + if (x < meshX - 1) { + Vector3f xp = new Vector3f((float)sizeX, (float)(heightMap[z][x+1] - heightMap[z][x]), 0.0f); + if (z < meshZ - 1) { + Vector3f zp = new Vector3f(0.0f, (float)(heightMap[z+1][x] - heightMap[z][x]), (float)sizeZ); + Vector3f v1 = new Vector3f(); + v1.cross(zp, xp); + v1.normalize(); + normal.add(v1); + } + if (z > 0) { + Vector3f zm = new Vector3f(0.0f, (float)(heightMap[z-1][x] - heightMap[z][x]), -(float)sizeZ); + Vector3f v1 = new Vector3f(); + v1.cross(xp, zm); + v1.normalize(); + normal.add(v1); + } + } + if (x > 0) { + Vector3f xm = new Vector3f(-(float)sizeX, (float)(heightMap[z][x-1] - heightMap[z][x]), 0.0f); + if (z < meshZ - 1) { + Vector3f zp = new Vector3f(0.0f, (float)(heightMap[z+1][x] - heightMap[z][x]), (float)sizeZ); + Vector3f v1 = new Vector3f(); + v1.cross(xm, zp); + v1.normalize(); + normal.add(v1); + } + if (z > 0) { + Vector3f zm = new Vector3f(0.0f, (float)(heightMap[z-1][x] - heightMap[z][x]), -(float)sizeZ); + Vector3f v1 = new Vector3f(); + v1.cross(zm, xm); + v1.normalize(); + normal.add(v1); + } + } + return normal; + } + + /** + * 衝突判定用のボリューム(ポリゴンと粗い判定用の直方体)を取得する + * @return 衝突判定用のボリューム列 + */ + public BoundingSurface[] getBoundingSurfaces() { + if (boundingSurfaces == null) { + if (triStripAttay == null) return null; + double coordinate1[] = new double[3]; + double coordinate2[] = new double[3]; + double coordinate3[] = new double[3]; + double coordinate4[] = new double[3]; + + // GeometryArrayの習得 + if (triStripAttay instanceof TriangleStripArray) { + // IndexedTriangleStripArray の場合 + // 8 x 8 メッシュ単位でまとめる + BoundingSurface surfaces[] = new BoundingSurface[((meshX + MESH_SIZE - 2) / MESH_SIZE) * ((meshZ + MESH_SIZE - 2) / MESH_SIZE)]; + int n = 0; + for (int j = 0; j < meshZ - 1; j += MESH_SIZE) { + for (int i = 0; i < meshX - 1; i += MESH_SIZE) { + BoundingSurface parent = new BoundingSurface(); + double lowY = 1.0; + double highY = -1.0; + for (int j2 = 0; j + j2 < meshZ - 1 && j2 < MESH_SIZE; j2++) { + for (int i2 = 0; i + i2 < meshX - 1 && i2 < MESH_SIZE; i2++) { + int index = (j + j2) * meshX * 2 + (i + i2) * 2; + triStripAttay.getCoordinates(index, coordinate1); + triStripAttay.getCoordinates(index + 1, coordinate2); + triStripAttay.getCoordinates(index + 2, coordinate3); + triStripAttay.getCoordinates(index + 3, coordinate4); + if (lowY > highY) { + lowY = highY = coordinate1[1]; + } else { + if (lowY > coordinate1[1]) { + lowY = coordinate1[1]; + } else if (highY < coordinate1[1]) { + highY = coordinate1[1]; + } + if (lowY > coordinate2[1]) { + lowY = coordinate2[1]; + } else if (highY < coordinate2[1]) { + highY = coordinate2[1]; + } + if (lowY > coordinate3[1]) { + lowY = coordinate3[1]; + } else if (highY < coordinate3[1]) { + highY = coordinate3[1]; + } + if (lowY > coordinate4[1]) { + lowY = coordinate4[1]; + } else if (highY < coordinate4[1]) { + highY = coordinate4[1]; + } + } + + // 1 x 1 メッシュ内のBoundingSurface + Vector3d v1 = new Vector3d(coordinate1); + Vector3d v2 = new Vector3d(coordinate2); + Vector3d v3 = new Vector3d(coordinate3); + Vector3d v4 = new Vector3d(coordinate4); + BoundingSurface bSurface = new BoundingSurface(); + bSurface.addVertex((Vector3d)v1.clone()); //1つめの三角形 + bSurface.addVertex((Vector3d)v2.clone()); + bSurface.addVertex((Vector3d)v3.clone()); + bSurface.setBounds(createBoundingPolytope(v1, v2, v3)); + parent.addChild(bSurface, false); + + bSurface = new BoundingSurface(); + bSurface.addVertex((Vector3d)v2.clone()); //2つめの三角形 + bSurface.addVertex((Vector3d)v4.clone()); + bSurface.addVertex((Vector3d)v3.clone()); + bSurface.setBounds(createBoundingPolytope(v2, v4, v3)); + parent.addChild(bSurface, true); + } + } + // 8 x 8 メッシュ単位のBoundingSurface + triStripAttay.getCoordinates(j * meshX * 2 + i * 2, coordinate1); + parent.setBounds(new BoundingBox(new Point3d(coordinate1[0], lowY, coordinate1[2]), + new Point3d(coordinate4[0], highY, coordinate4[2]))); + surfaces[n] = parent; + n++; + } + } + boundingSurfaces = surfaces; + } else { + return null; + } + } + return boundingSurfaces; + } + + public double getHeight(double x, double z) { + int i = (int)((x - origin.getX()) / sizeX); + int j = (int)((z - origin.getZ()) / sizeZ); + if (i >= meshX || i < 0 || j >= meshZ || j < 0) return 0.0; + + int index = j * meshX * 2 + i * 2; + double coordinate1[] = new double[3]; + double coordinate2[] = new double[3]; + double coordinate3[] = new double[3]; + double coordinate4[] = new double[3]; + triStripAttay.getCoordinates(index, coordinate1); + triStripAttay.getCoordinates(index + 1, coordinate2); + triStripAttay.getCoordinates(index + 2, coordinate3); + triStripAttay.getCoordinates(index + 3, coordinate4); + Vector3d v1 = new Vector3d(coordinate1); + Vector3d v2 = new Vector3d(coordinate2); + Vector3d v3 = new Vector3d(coordinate3); + Vector3d v4 = new Vector3d(coordinate4); + Vector3d p1 = new Vector3d(x, 0.0, z); + Vector3d p2 = new Vector3d(x, 1.0, z); + + double x2 = x - (double)i * sizeX - origin.getX(); + double z2 = z - (double)j * sizeZ - origin.getZ(); + if (x2 < GeometryUtility.TOLERANCE || (meshZ - z2) / x2 > meshZ / meshX) { + // 1つめの三角形を通る場合 + Vector3d crossPoint = GeometryUtility.intersect(GeometryUtility.createPlane(v1, v2, v3), p1, p2); + return crossPoint.getY(); + } else { + // 2つめの三角形を通る場合 + Vector3d crossPoint = GeometryUtility.intersect(GeometryUtility.createPlane(v2, v4, v3), p1, p2); + return crossPoint.getY(); + } + } +} + +// +// ********** IndexedTriangleStripArray を使うバージョン ********** +// (IndexedTriangleStripArrayを用いるとメモリを節約できるが、シェーディングができないので注意) +// +//public class Terrain3D extends BaseObject3D { +// static final int MESH_SIZE = 8; +// private Position3D origin; +// private double sizeX = 0; +// private double sizeZ = 0; +// private double heightMap[][] = null; +// private int meshX = 0; +// private int meshZ = 0; +// private IndexedTriangleStripArray triStripAttay = null; +// +// public Terrain3D(Position3D origin, double sizeX, double sizeZ, double heightMap[][], Appearance a) { +// super(); +// this.origin = origin; +// this.sizeX = sizeX; +// this.sizeZ = sizeZ; +// this.heightMap = heightMap; +// meshX = heightMap[0].length; +// meshZ = heightMap.length; +// +// int stripVertexCounts[] = new int[meshZ - 1]; +// for (int n = 0; n < meshZ - 1; n++) { +// stripVertexCounts[n] = meshX * 2; +// } +// triStripAttay = new IndexedTriangleStripArray(meshX * meshZ, +// IndexedTriangleStripArray.COORDINATES | IndexedTriangleStripArray.NORMALS, +// meshX * 2 * (meshZ - 1), +// stripVertexCounts); +// int index = 0, index2 = 0, i1, i2; +// for (int z = 0; z < meshZ; z++) { +// for (int x = 0; x < meshX; x++) { +// // 頂点座標の設定 +// triStripAttay.setCoordinate(index, +// new Point3d(origin.getX() + sizeX * (double)x, +// origin.getY() + heightMap[z][x], +// origin.getZ() + sizeZ * (double)z)); +// // 頂点の法線ベクトルの設定 +// Vector3f normal = new Vector3f(); +// if (x < meshX - 1) { +// Vector3f xp = new Vector3f((float)sizeX, (float)(heightMap[z][x+1] - heightMap[z][x]), 0.0f); +// if (z < meshZ - 1) { +// Vector3f zp = new Vector3f(0.0f, (float)(heightMap[z+1][x] - heightMap[z][x]), (float)sizeZ); +// Vector3f v1 = new Vector3f(); +// v1.cross(zp, xp); +// v1.normalize(); +// normal.add(v1); +// } +// if (z > 0) { +// Vector3f zm = new Vector3f(0.0f, (float)(heightMap[z-1][x] - heightMap[z][x]), -(float)sizeZ); +// Vector3f v1 = new Vector3f(); +// v1.cross(xp, zm); +// v1.normalize(); +// normal.add(v1); +// } +// } +// if (x > 0) { +// Vector3f xm = new Vector3f(-(float)sizeX, (float)(heightMap[z][x-1] - heightMap[z][x]), 0.0f); +// if (z < meshZ - 1) { +// Vector3f zp = new Vector3f(0.0f, (float)(heightMap[z+1][x] - heightMap[z][x]), (float)sizeZ); +// Vector3f v1 = new Vector3f(); +// v1.cross(xm, zp); +// v1.normalize(); +// normal.add(v1); +// } +// if (z > 0) { +// Vector3f zm = new Vector3f(0.0f, (float)(heightMap[z-1][x] - heightMap[z][x]), -(float)sizeZ); +// Vector3f v1 = new Vector3f(); +// v1.cross(zm, xm); +// v1.normalize(); +// normal.add(v1); +// } +// } +// normal.normalize(); +// triStripAttay.setNormal(index, normal); +// index++; +// // メッシュごとに三角形を2つづつ生成 +// if (z < meshZ - 1) { +// i1 = z * meshX + x; +// i2 = (z + 1) * meshX + x; +// triStripAttay.setCoordinateIndices(index2, new int[]{i1, i2}); +// index2 += 2; +// } +// } +// } +// Appearance a2 = (Appearance)a.cloneNodeComponent(true); +// a2.setCapability(Appearance.ALLOW_MATERIAL_READ); +// a2.setCapability(Appearance.ALLOW_TEXTURE_READ); +// a2.setCapability(Appearance.ALLOW_TEXTURE_WRITE); +// a2.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_READ); +// a2.setCapability(Appearance.ALLOW_TEXTURE_ATTRIBUTES_WRITE); +// Shape3D shape = new Shape3D(triStripAttay, a2); +// shape.setCapability(Shape3D.ALLOW_APPEARANCE_READ); +// shape.setCapability(Shape3D.ALLOW_APPEARANCE_WRITE); +// center.addChild(shape); +// } +// +// public BoundingSurface[] getBoundingSurfaces() { +// if (boundingSurfaces == null) { +// if (triStripAttay == null) return null; +// double coordinate1[] = new double[3]; +// double coordinate2[] = new double[3]; +// double coordinate3[] = new double[3]; +// double coordinate4[] = new double[3]; +// +// // GeometryArrayの習得 +// if (triStripAttay instanceof IndexedTriangleStripArray) { +// // IndexedTriangleStripArray の場合 +// // 8 x 8 メッシュ単位でまとめる +// BoundingSurface surfaces[] = new BoundingSurface[((meshX + MESH_SIZE - 2) / MESH_SIZE) * ((meshZ + MESH_SIZE - 2) / MESH_SIZE)]; +// int n = 0; +// for (int j = 0; j < meshZ - 1; j += MESH_SIZE) { +// for (int i = 0; i < meshX - 1; i += MESH_SIZE) { +// BoundingSurface parent = new BoundingSurface(); +// double lowY = 1.0; +// double highY = -1.0; +// for (int j2 = 0; j + j2 < meshZ - 1 && j2 < MESH_SIZE; j2++) { +// for (int i2 = 0; i + i2 < meshX - 1 && i2 < MESH_SIZE; i2++) { +// int index = (j + j2) * meshX * 2 + (i + i2) * 2; +// triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index), coordinate1); +// triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index+1), coordinate2); +// triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index+2), coordinate3); +// triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index+3), coordinate4); +// if (lowY > highY) { +// lowY = highY = coordinate1[1]; +// } else { +// if (lowY > coordinate1[1]) { +// lowY = coordinate1[1]; +// } else if (highY < coordinate1[1]) { +// highY = coordinate1[1]; +// } +// if (lowY > coordinate2[1]) { +// lowY = coordinate2[1]; +// } else if (highY < coordinate2[1]) { +// highY = coordinate2[1]; +// } +// if (lowY > coordinate3[1]) { +// lowY = coordinate3[1]; +// } else if (highY < coordinate3[1]) { +// highY = coordinate3[1]; +// } +// if (lowY > coordinate4[1]) { +// lowY = coordinate4[1]; +// } else if (highY < coordinate4[1]) { +// highY = coordinate4[1]; +// } +// } +// +// // 1 x 1 メッシュ内のBoundingSurface +// Vector3d v1 = new Vector3d(coordinate1); +// Vector3d v2 = new Vector3d(coordinate2); +// Vector3d v3 = new Vector3d(coordinate3); +// Vector3d v4 = new Vector3d(coordinate4); +// BoundingSurface bSurface = new BoundingSurface(); +// bSurface.addVertex((Vector3d)v1.clone()); //1つめの三角形 +// bSurface.addVertex((Vector3d)v2.clone()); +// bSurface.addVertex((Vector3d)v3.clone()); +// bSurface.setBounds(createBoundingPolytope(v1, v2, v3)); +// parent.addChild(bSurface, false); +// +// bSurface = new BoundingSurface(); +// bSurface.addVertex((Vector3d)v2.clone()); //2つめの三角形 +// bSurface.addVertex((Vector3d)v4.clone()); +// bSurface.addVertex((Vector3d)v3.clone()); +// bSurface.setBounds(createBoundingPolytope(v2, v4, v3)); +// parent.addChild(bSurface, true); +// } +// } +// // 8 x 8 メッシュ単位のBoundingSurface +// triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(j * meshX * 2 + i * 2), coordinate1); +// parent.setBounds(new BoundingBox(new Point3d(coordinate1[0], lowY, coordinate1[2]), +// new Point3d(coordinate4[0], highY, coordinate4[2]))); +// surfaces[n] = parent; +// n++; +// } +// } +// boundingSurfaces = surfaces; +// } else { +// return null; +// } +// } +// return boundingSurfaces; +// } +// +// public double getHeight(double x, double z) { +// int i = (int)((x - origin.getX()) / sizeX); +// int j = (int)((z - origin.getZ()) / sizeZ); +// if (i >= meshX || i < 0 || j >= meshZ || j < 0) return 0.0; +// +// int index = j * meshX * 2 + i * 2; +// double coordinate1[] = new double[3]; +// double coordinate2[] = new double[3]; +// double coordinate3[] = new double[3]; +// double coordinate4[] = new double[3]; +// triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index), coordinate1); +// triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index+1), coordinate2); +// triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index+2), coordinate3); +// triStripAttay.getCoordinates(triStripAttay.getCoordinateIndex(index+3), coordinate4); +// Vector3d v1 = new Vector3d(coordinate1); +// Vector3d v2 = new Vector3d(coordinate2); +// Vector3d v3 = new Vector3d(coordinate3); +// Vector3d v4 = new Vector3d(coordinate4); +// Vector3d p1 = new Vector3d(x, 0.0, z); +// Vector3d p2 = new Vector3d(x, 1.0, z); +// +// double x2 = x - (double)i * sizeX - origin.getX(); +// double z2 = z - (double)j * sizeZ - origin.getZ(); +// if (x2 < GeometryUtility.TOLERANCE || (meshZ - z2) / x2 > meshZ / meshX) { +// // 1つめの三角形を通る場合 +// Vector3d crossPoint = GeometryUtility.intersect(GeometryUtility.createPlane(v1, v2, v3), p1, p2); +// return crossPoint.getY(); +// } else { +// // 2つめの三角形を通る場合 +// Vector3d crossPoint = GeometryUtility.intersect(GeometryUtility.createPlane(v2, v4, v3), p1, p2); +// return crossPoint.getY(); +// } +// } +//} diff --git a/src/main/java/framework/model3D/UndoBuffer.java b/src/main/java/framework/model3D/UndoBuffer.java new file mode 100644 index 0000000..b258504 --- /dev/null +++ b/src/main/java/framework/model3D/UndoBuffer.java @@ -0,0 +1,49 @@ +package framework.model3D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Hashtable; + + +public class UndoBuffer { + + @SuppressWarnings("unchecked") + private ArrayList> list = new ArrayList>(); + @SuppressWarnings("unchecked") + private Hashtable nowCondition = new Hashtable(); + private static final int max = 3; + + UndoBuffer() { + } + + UndoBuffer(UndoBuffer another) { + this.list = (ArrayList>)another.list.clone(); + this.nowCondition = (Hashtable)another.nowCondition.clone(); + } + + public void push(Property3D p) { + nowCondition.put(p.getClass(), p.clone()); + } + + @SuppressWarnings("unchecked") + public void setUndoMark() { + if(list.size() == max) { + list.remove(max - 1); + } + list.add(0, (Hashtable) nowCondition.clone()); + } + + @SuppressWarnings("unchecked") + public Collection undo() { + if(list.size() != 0) { + nowCondition = list.remove(0); + return nowCondition.values(); + } + else { + return new Hashtable().values(); + } + } + + public void clear() { + + } +} diff --git a/src/main/java/framework/model3D/Universe.java b/src/main/java/framework/model3D/Universe.java new file mode 100644 index 0000000..2f417a0 --- /dev/null +++ b/src/main/java/framework/model3D/Universe.java @@ -0,0 +1,433 @@ +package framework.model3D; + +import framework.physics.Ground; + +import javax.media.j3d.*; +import javax.vecmath.Color3f; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Stack; + + +/** + * 影、反射マッピング、バンプマッピングなどがレンダリングできる SimpleUniverse + * (すべての物体を配置した後に、必ず compile() を呼ばないといけないことに注意!) + * @author 新田直也 + */ +public class Universe extends VirtualUniverse { + private Locale locale = null; + private BranchGroup root = null; + private BranchGroup additionalRoot = null; + private ArrayList lights = new ArrayList(); + private BackgroundBox skyBox = null; + // private ArrayList backgrounds = new ArrayList(); + private double shadowDepth = 100.0; + private ArrayList occluders = new ArrayList(); + private ArrayList shadows = new ArrayList(); + private ArrayList receivers = new ArrayList(); + private ArrayList extraObjects = new ArrayList(); + + private Ground ground = null; + private ArrayList movableList = new ArrayList<>(); + + public Universe() { + super(); + locale = new Locale(this); + root = new BranchGroup(); + additionalRoot = new BranchGroup(); + additionalRoot.setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND); + additionalRoot.setCapability(BranchGroup.ALLOW_CHILDREN_READ); + additionalRoot.setCapability(BranchGroup.ALLOW_CHILDREN_WRITE); + shadowDepth = 100.0; + } + + /** + * モデルのルートを設定する + * @param root + */ + public void setRoot(BranchGroup root) { + removeRoot(); + this.root = root; + } + + /** + * モデルのルートを取得する + * @return モデルのルートに相当する SceneGraph + */ + public BranchGroup getRoot() { + return root; + } + + /** + * モデルのルートを削除する + */ + public void removeRoot() { + if (root == null) return; + Enumeration branchGroups = locale.getAllBranchGraphs(); + while (branchGroups.hasMoreElements()) { + if (branchGroups.nextElement() == root) { + locale.removeBranchGraph(root); + root = null; + break; + } + } + } + + public void addBranchGraph(BranchGroup g) { + locale.addBranchGraph(g); + } + + /** + * オブジェクトを配置する + * @param obj 配置するオブジェクト + */ + public void place(Node obj) { + if (!root.isCompiled()) { + root.addChild(obj); + } else { + additionalRoot.addChild(obj); + } + } + + /** + * オブジェクトを配置する + * @param obj 配置するオブジェクト + */ + public void place(Placeable obj) { + if (obj instanceof Ground) { + ground = (Ground) obj; + } + if (obj instanceof Movable) { + movableList.add((Movable) obj); + } + + BaseObject3D body = obj.getBody(); + if (body.isReflectionMappingApplied() || body.isBumpMappingApplied()) { + extraObjects.add(body); + } else { + place(obj.getTransformGroupToPlace()); + } + } + + /** + * 後で取り除けるようにオブジェクトを配置する + * @param obj 配置するオブジェクト + */ + public void placeDisplacable(Node obj) { + BranchGroup objRoot; + if (obj.getParent() != null + && obj.getParent() instanceof BranchGroup) { + objRoot = (BranchGroup) obj.getParent(); + } else { + objRoot = new BranchGroup(); + objRoot.setCapability(BranchGroup.ALLOW_DETACH); + objRoot.addChild(obj); + } + additionalRoot.addChild(objRoot); + } + + /** + * 後で取り除けるようにオブジェクトを配置する + * @param obj 配置するオブジェクト + */ + public void placeDisplacable(Placeable obj) { + if (obj instanceof Ground) { + ground = (Ground) obj; + } + if (obj instanceof Movable) { + movableList.add((Movable) obj); + } + + BaseObject3D body = obj.getBody(); + if (body != null && (body.isReflectionMappingApplied() || body.isBumpMappingApplied())) { + extraObjects.add(body); + } else { + placeDisplacable(obj.getTransformGroupToPlace()); + } + } + + /** + * 影付きで配置する + * @param obj 配置するオブジェクト + */ + public void placeAsAnOcculuder(Placeable obj) { + BaseObject3D body = obj.getBody(); + if (body instanceof Object3D) { + addShadowOcculuder((Object3D) body); + } else { + place(obj); + } + } + + /** + * 他のオブジェクトの影が落ちるようにオブジェクトを配置する + * @param obj 配置するオブジェクト + */ + public void placeAsAReceiver(Placeable obj) { + addShadowReceiver(obj.getBody(), new Color3f(0.5f, 0.5f, 0.5f)); + } + + /** + * 他のオブジェクトの影が落ちるようにオブジェクトを表示する + * @param obj 表示するオブジェクト + * @param shadowColor 影の色 + */ + public void placeAsAReceiver(Placeable obj, Color3f shadowColor) { + addShadowReceiver(obj.getBody(), shadowColor); + } + + /** + * 光源の追加 + * @param light 追加する光源 + */ + public void placeLight(Light light) { + root.addChild(light); + getLights().add((Light) light.cloneTree()); + } + + /** + * 光源の追加(影つき) + * @param light 追加する光源 + * @param shadowDepth 影の深さ + */ + public void placeLight(Light light, double shadowDepth) { + root.addChild(light); + getLights().add((Light) light.cloneTree()); + this.shadowDepth = shadowDepth; + } + + /** + * スカイボックスの追加 + * @param skyBox 追加するスカイボックス + */ + public void placeSkyBox(BackgroundBox skyBox) { + root.addChild(skyBox); + this.skyBox = skyBox; + } + + /** + * オブジェクトを可能ならば取り除く + * @param obj 取り除くオブジェクト + */ + public void displace(Node obj) { + Node parent = obj.getParent(); + if (parent == root) return; + if (parent == additionalRoot) { + additionalRoot.removeChild(obj); + } else if (parent instanceof BranchGroup + && parent.getCapability(BranchGroup.ALLOW_DETACH) + && parent.getParent() == additionalRoot) { + additionalRoot.removeChild(parent); + } + } + + /** + * オブジェクトを可能ならば取り除く + * @param obj 取り除くオブジェクト + */ + public void displace(Placeable obj) { + if (movableList.contains(obj)) { + movableList.remove(obj); + } + + BaseObject3D body = obj.getBody(); + if (occluders.contains(body)) { + occluders.remove(body); + } + if (shadows.contains(body)) { + shadows.remove(body); + } + if (receivers.contains(body)) { + receivers.remove(body); + } + if (extraObjects.contains(body)) { + extraObjects.remove(body); + } + displace(obj.getTransformGroupToPlace()); + } + + public ArrayList getLights() { + return lights; + } + + public BackgroundBox getSkyBox() { + return skyBox; + } + + private void addShadowOcculuder(Object3D occluder) { +// occluder.view(this); // occluder 自身の表示 + ShadowVolumeVisitor shadowVolumeVisitor = + new ShadowVolumeVisitor(getLights(), shadowDepth); // ShadowVolume の生成 + occluder.accept(shadowVolumeVisitor); // occluder 内部で ShadowVolume が保持される + occluders.add(occluder); + } + + private void addShadowReceiver(BaseObject3D receiverObject, Color3f shadowColor) { + BaseObject3D receiverObjectBody = receiverObject.duplicate(); + + // 元の BaseObject3D を影表示用にする + ArrayList aps = receiverObject.getAppearances(); + Material m = new Material(); + m.setDiffuseColor(shadowColor); // 影の色 + m.setAmbientColor(0.0f, 0.0f, 0.0f); + m.setSpecularColor(0.0f, 0.0f, 0.0f); + m.setShininess(1.0f); + RenderingAttributes ra = new RenderingAttributes(); + ra.setStencilEnable(false); // ステンシルバッファを使わない + ra.setDepthBufferWriteEnable(true); // 通常通りZバッファに書き込む + ra.setDepthTestFunction(RenderingAttributes.LESS_OR_EQUAL); // 通常通りZバッファで判定する + for (int n = 0; n < aps.size(); n++) { + Appearance ap = aps.get(n); + ap.setMaterial(m); + ap.setRenderingAttributes(ra); + } + + shadows.add(receiverObject); + + // 複製した BaseObject3D を通常表示用(ただし影の部分は描画しない)にする + ArrayList aps2 = receiverObjectBody.getAppearances(); + RenderingAttributes ra2 = new RenderingAttributes(); + ra2.setStencilEnable(true); // ステンシルバッファを使う + ra2.setDepthBufferWriteEnable(true); + ra2.setDepthTestFunction(RenderingAttributes.LESS_OR_EQUAL); // 影の上に上書きする + ra2.setStencilFunction(RenderingAttributes.GREATER_OR_EQUAL, 0, ~0); // ステンシルバッファの値が1以上のとき描画しない + ra2.setStencilOp(RenderingAttributes.STENCIL_REPLACE, // ステンシルバッファをクリアする + RenderingAttributes.STENCIL_REPLACE, + RenderingAttributes.STENCIL_REPLACE); +// TransparencyAttributes ta2 = new TransparencyAttributes(); +// ta2.setTransparencyMode(TransparencyAttributes.BLENDED); +// ta2.setTransparency(0.0001f); + for (int n = 0; n < aps2.size(); n++) { + Appearance ap2 = aps2.get(n); + ap2.setRenderingAttributes(ra2); +// ap2.setTransparencyAttributes(ta2); + } + + receivers.add(receiverObjectBody); + } + + public void update() { + ShadowVolumeVisitor shadowVolumeVisitor = + new ShadowVolumeVisitor(); // ShadowVolume の 更新 + for (int n = 0; n < occluders.size(); n++) { + occluders.get(n).accept(shadowVolumeVisitor); + } + } + + public void update(long interval) { + for (int i = 0; i < movableList.size(); i++) { + Movable movable = movableList.get(i); + movable.motion(interval, ground); + } + } + + /** + * Mixedモードレンダリングを用いて影の描画を行う + * (影が落ちるオブジェクト(レシーバ)の影部分、シャドウボリューム) + * @param graphicsContext3D + */ + public void preRender(IViewer3D viewer) { + update(); + + viewer.update(lights, skyBox); + } + + /** + * Mixedモードレンダリングを用いて影の描画を行う + * (影が落ちるオブジェクトの影以外の部分) + * @param graphicsContext3D + */ + public void renderField(IViewer3D viewer) { + // *** レンダリングは以下の順番でしかうまくいかない *** + // たとえば、シャドウボリュームをレンダリングした後にレシーバの影以外の部分をレンダリングすると、 + // BackgroundBoxの描画がなぜか汚くなってしまう。 + + // 1. レシーバの影部分 + Transform3D trans = new Transform3D(); + for (int n = 0; n < shadows.size(); n++) { + BaseObject3D obj = shadows.get(n); + if (obj instanceof Object3D) { + ((Object3D) obj).accept(new ObjectRenderer(viewer)); + } else { + obj.center.getTransform(trans); + viewer.setModelTransform(trans); + viewer.draw(obj); + } + } + + // 2. レシーバの影以外の部分 + for (int n = 0; n < receivers.size(); n++) { + BaseObject3D obj = receivers.get(n); + if (obj instanceof Object3D) { + ((Object3D) obj).accept(new ObjectRenderer(viewer)); + } else { + obj.center.getTransform(trans); + viewer.setModelTransform(trans); + viewer.draw(obj); + } + } + + // 3. シャドウボリュームの表面(occuluder内部に積んでいる) + // 4. シャドウボリュームの裏面(occuluder内部に積んでいる) + for (int n = 0; n < occluders.size(); n++) { + Object3D occuluder = occluders.get(n); + occuluder.accept(new ObjectRenderer(viewer)); + } + + // 特殊なレンダリングが必要となるオブジェクト + for (int n = 0; n < extraObjects.size(); n++) { + BaseObject3D extra = extraObjects.get(n); + if (extra instanceof Object3D) { + ((Object3D) extra).accept(new ObjectRenderer(viewer)); + } else { + extra.center.getTransform(trans); + viewer.setModelTransform(trans); + viewer.draw(extra); + } + } + } + + public void postRender(IViewer3D viewer) { + } + + public void compile() { + root.compile(); + locale.addBranchGraph(root); + locale.addBranchGraph(additionalRoot); + } + + private class ObjectRenderer extends ObjectVisitor { + Stack transforms = new Stack(); + IViewer3D viewer = null; + + public ObjectRenderer(IViewer3D viewer) { + Transform3D t = new Transform3D(); + transforms.push(t); + this.viewer = viewer; + } + + @Override + public void preVisit(Object3D obj) { + Transform3D t = new Transform3D(transforms.peek()); + Transform3D t2 = new Transform3D(); + obj.pos.getTransform(t2); + t.mul(t2); + obj.rot.getTransform(t2); + t.mul(t2); + obj.scale.getTransform(t2); + t.mul(t2); + obj.center.getTransform(t2); + t.mul(t2); + transforms.push(t); + } + + @Override + public void postVisit(Object3D obj) { + Transform3D t = transforms.pop(); + if (!obj.hasChildren()) { + viewer.setModelTransform(t); + viewer.draw(obj); + } + } + } +} diff --git a/src/main/java/framework/network/CallBack.java b/src/main/java/framework/network/CallBack.java new file mode 100644 index 0000000..e56621d --- /dev/null +++ b/src/main/java/framework/network/CallBack.java @@ -0,0 +1,5 @@ +package framework.network; + +public interface CallBack { + public void onResponse(String response); +} diff --git a/src/main/java/framework/network/HttpAsyncConnection.java b/src/main/java/framework/network/HttpAsyncConnection.java new file mode 100644 index 0000000..1a89744 --- /dev/null +++ b/src/main/java/framework/network/HttpAsyncConnection.java @@ -0,0 +1,166 @@ +package framework.network; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import android.os.AsyncTask; +import android.util.Log; + +abstract public class HttpAsyncConnection extends AsyncTask { + + private HttpURLConnection conn = null; + private String baseUrl = null; + private String queryParams = ""; + private String pathParams = ""; + private String formParams = ""; + private CallBack callBack = null; + + + private String method; + private static String clientSessionID = null; + + public HttpAsyncConnection(String url) { + baseUrl = url; + } + + + public void doPost() { + setMethod("POST"); + execute(); + } + + public void doGet() { + setMethod("GET"); + execute(); + } + + public void doPut() { + setMethod("PUT"); + execute(); + } + + public void doDelete() { + setMethod("DELETE"); + execute(); + } + + public void setCallBack(CallBack callBack) { + this.callBack = callBack; + } + + private void setMethod(String method) { + this.method = method; + } + + @Override + protected String doInBackground(Void... urls) { + doAnything(); + return doReceive(); + } + + /* (non-Javadoc) + * @see android.os.AsyncTask#onPostExecute(java.lang.Object) + */ + public void onPostExecute(String response) { + try { + if (callBack != null) callBack.onResponse(response); + } catch (Exception e) { + // TODO: handle exception + e.printStackTrace(); + } + } + + + // request + public void doAnything() { + try { + if(conn == null) { + if(queryParams == null || queryParams.length() == 0){ + conn = (HttpURLConnection) new URL(baseUrl + pathParams).openConnection(); + } else { + conn = (HttpURLConnection) new URL(baseUrl + pathParams + "?" + queryParams).openConnection(); + } + } + conn.setReadTimeout(10000 /* milliseconds */); + conn.setConnectTimeout(15000 /* milliseconds */); + // POST or GET or PUT or DELETE + conn.setRequestMethod(method); + + if (formParams.length() > 0) { + conn.setDoOutput(true); + if(clientSessionID != null) { + conn.setRequestProperty("Cookie", clientSessionID); + } + OutputStream out = conn.getOutputStream(); + out.write(formParams.getBytes("UTF-8")); + out.flush(); + out.close(); + formParams = ""; + } + pathParams = ""; + queryParams = ""; + + conn.connect(); + + if(clientSessionID == null ) { + clientSessionID = conn.getHeaderField("Set-Cookie"); + + } + } catch (IOException e) { + notConnection(); + e.printStackTrace(); + } + } + + // response + public String doReceive() { + BufferedReader reader; + try { + reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String line; + StringBuilder builder = new StringBuilder(); + while((line = reader.readLine()) != null) + builder.append(line); + reader.close(); + if (conn != null) conn.disconnect(); + return builder.toString(); + } catch (IOException e) { + e.printStackTrace(); + if (conn != null) conn.disconnect(); + return null; + } + + } + + public void notConnection(){}; + + /** + * param set + * @param key + * @param value + */ + public void addQueryParam(String key, String value) { + if(queryParams.length() > 0) { + queryParams += ("&"); + } + queryParams += (key + "=" + value); + } + + public void addFormParam(String key, String value) { + if(formParams.length() > 0) { + formParams += "&"; + } + formParams += (key + "=" + value); + } + + public void addPathParam(String param){ + pathParams += "/" + param; + } +} diff --git a/src/main/java/framework/physics/AngularVelocity3D.java b/src/main/java/framework/physics/AngularVelocity3D.java new file mode 100644 index 0000000..ea13536 --- /dev/null +++ b/src/main/java/framework/physics/AngularVelocity3D.java @@ -0,0 +1,86 @@ +package framework.physics; + +import framework.model3D.Object3D; +import framework.model3D.Property3D; + +import javax.vecmath.AxisAngle4d; +import javax.vecmath.Vector3d; + + +public class AngularVelocity3D extends Property3D { + private double x; + private double y; + private double z; + + public AngularVelocity3D(AngularVelocity3D w) { + x = w.x; + y = w.y; + z = w.z; + } + + public AngularVelocity3D(double x,double y,double z){ + this.x = x; + this.y = y; + this.z = z; + } + + public AngularVelocity3D(){ + x = 0.0; + y = 0.0; + z = 0.0; + } + + public void applyTo(Object3D o){ + ((Solid3D)o).setAngularVelocity(this); + } + + public double getX() { + // TODO Auto-generated method stub + return this.x; + } + + public double getY() { + // TODO Auto-generated method stub + return this.y; + } + + public double getZ() { + // TODO Auto-generated method stub + return this.z; + } + + public AngularVelocity3D add(double x, double y, double z){ + this.x += x; + this.y += y; + this.z += z; + return this; + } + + public AngularVelocity3D add(Vector3d v) { + // TODO Auto-generated method stub + this.x += v.x; + this.y += v.y; + this.z += v.z; + return this; + } + + public Vector3d getVector3d(){ + return new Vector3d(x,y,z); + } + + public AxisAngle4d getAxisAngle4d() { + double l = getVector3d().length(); + if (l <= 0.00001) { + return new AxisAngle4d(0, 0, 1.0, 0.0); + } + return new AxisAngle4d(x / l, y / l, z / l, l); + } + + @Override + public Property3D clone() { + // TODO Auto-generated method stub + return new AngularVelocity3D(this); + } +} + + diff --git a/src/main/java/framework/physics/BoundingBoxVisitor.java b/src/main/java/framework/physics/BoundingBoxVisitor.java new file mode 100644 index 0000000..b1e86f9 --- /dev/null +++ b/src/main/java/framework/physics/BoundingBoxVisitor.java @@ -0,0 +1,93 @@ +package framework.physics; + +import framework.model3D.OBB; +import framework.model3D.Object3D; +import framework.model3D.ObjectVisitor; + +import javax.media.j3d.BoundingSphere; +import java.util.ArrayList; + + +public class BoundingBoxVisitor extends ObjectVisitor { + private ArrayList obbList = new ArrayList(); // 全構成要素のOBBのリスト + private ArrayList bsStack = new ArrayList(); // オブジェクトの階層毎のBoundingSphereのスタック + private String partName = null; // 部品を指定する場合に使う + private boolean inPart = false; + + public BoundingBoxVisitor() { + partName = null; + } + + public BoundingBoxVisitor(String partName) { + this.partName = partName; + } + + public void preVisit(Object3D obj) { + pushTransform(obj); + if (partName != null && obj.name.equals(partName)) { + inPart = true; + } + if (obj.hasChildren() && obj.bs == null) { + // 子供がいる場合、下の階層用にnullをpushする + bsStack.add(null); + } + } + + public void postVisit(Object3D obj) { + int pattern = 2; + if (!obj.hasChildren()) { + // 葉の場合 + OBB obb = obj.getOBB(pattern); + if (obb != null) { + if (obj.bs == null) { + obj.bs = obb.getBoundingSphere(); + } + + obb = (OBB)obb.clone(); + BoundingSphere bs = (BoundingSphere)obj.bs.clone(); + for (int i = stackList.size() - 1; i >= 0; i--) { + obb.transform(stackList.get(i)); + bs.transform(stackList.get(i)); + } + if (partName == null || partName.length() == 0 || inPart) { + obbList.add(obb); // Transform3Dを適応させたBoundsをboundsListに追加 + int stackTop = bsStack.size() - 1; + if (bs != null && stackTop >= 0) { + if (bsStack.get(stackTop) == null) { + // その階層の最初のオブジェクトの場合、nullを置き換え + bsStack.set(stackTop, bs); + } else { + // その階層の2番目以降のオブジェクトの場合、結合 + bsStack.get(stackTop).combine(bs); + } + } + } + } + } else { + // 子供がいる場合 + int stackTop = bsStack.size() - 1; + if (obj.bs == null) { + // 下の階層の結合結果をpopして利用する + obj.bs = bsStack.remove(stackTop); + stackTop--; + } + if (obj.bs != null && stackTop >= 0) { + if (bsStack.get(stackTop) == null) { + // その階層の最初のオブジェクトの場合、nullを置き換え + bsStack.set(stackTop, obj.bs); + } else { + // その階層の2番目以降のオブジェクトの場合、結合 + bsStack.get(stackTop).combine(obj.bs); + } + } + } + popTransform(); + if (partName != null && obj.name.equals(partName)) { + inPart = false; + } + } + + public ArrayList getObbList() { + return obbList; + } +} diff --git a/src/main/java/framework/physics/BoundingSurfaceVisitor.java b/src/main/java/framework/physics/BoundingSurfaceVisitor.java new file mode 100644 index 0000000..d2287be --- /dev/null +++ b/src/main/java/framework/physics/BoundingSurfaceVisitor.java @@ -0,0 +1,59 @@ +package framework.physics; + +import framework.model3D.BaseObject3D; +import framework.model3D.BoundingSurface; +import framework.model3D.Object3D; +import framework.model3D.ObjectVisitor; + +import java.util.ArrayList; + +/** + * 地面の衝突判定用のボリュームを生成するためのビジター + * @author 新田直也 + * + */ +public class BoundingSurfaceVisitor extends ObjectVisitor { + private ArrayList boundingSurfaceList = new ArrayList(); + public BoundingSurfaceVisitor() { + boundingSurfaceList.add(new BoundingSurface()); + } + + public void preVisit(Object3D obj) { + pushTransform(obj); + if (obj.hasChildren()) { + boundingSurfaceList.add(new BoundingSurface()); + } + } + + public void postVisit(Object3D obj) { + if (!obj.hasChildren()) { + // 葉の場合 + BoundingSurface[] s = (BoundingSurface[]) obj.getBoundingSurfaces().clone(); + for (int i = 0; i < s.length; i++) { + s[i] = (BoundingSurface) s[i].clone(); + for (int j = stackList.size() - 1; j >= 0; j--) { + s[i].transform(stackList.get(j)); + } + boundingSurfaceList.get(boundingSurfaceList.size() - 1).addChild(s[i], true); // Transform3Dを適応させたBoundsをsurfaceListに追加 + } + } else { + BoundingSurface child = boundingSurfaceList.remove(boundingSurfaceList.size() - 1); + BoundingSurface parent = boundingSurfaceList.get(boundingSurfaceList.size() - 1); + parent.addChild(child, true); + } + popTransform(); + } + + public void baseVisit(BaseObject3D obj) { + BoundingSurface parent = boundingSurfaceList.get(boundingSurfaceList.size() - 1); + BoundingSurface[] s = (BoundingSurface[]) obj.getBoundingSurfaces().clone(); + for (int i = 0; i < s.length; i++) { + s[i] = (BoundingSurface) s[i].clone(); + parent.addChild(s[i], true); + } + } + + public BoundingSurface getBoundingSurface() { + return boundingSurfaceList.get(0); + } +} diff --git a/src/main/java/framework/physics/Force3D.java b/src/main/java/framework/physics/Force3D.java new file mode 100644 index 0000000..66a1171 --- /dev/null +++ b/src/main/java/framework/physics/Force3D.java @@ -0,0 +1,35 @@ +package framework.physics; +import javax.vecmath.Vector3d; + + +public class Force3D { + double x; + double y; + double z; + public static final Force3D ZERO = new Force3D( 0.0, 0.0, 0.0); + + public Force3D(double x,double y,double z){ + this.x = x; + this.y = y; + this.z = z; + } + + public Force3D(Vector3d v) { + this.x = v.x; + this.y = v.y; + this.z = v.z; + } + public Vector3d getVector3d(){ + return new Vector3d(x,y,z); + } + + public void add(Force3D f) { + x += f.x; + y += f.y; + z += f.z; + } + + public double getSeverity() { + return getVector3d().length(); + } +} diff --git a/src/main/java/framework/physics/Ground.java b/src/main/java/framework/physics/Ground.java new file mode 100644 index 0000000..fe5cdcc --- /dev/null +++ b/src/main/java/framework/physics/Ground.java @@ -0,0 +1,57 @@ +package framework.physics; + +import framework.model3D.BaseObject3D; +import framework.model3D.BoundingSurface; +import framework.model3D.Object3D; +import framework.model3D.Placeable; + +import javax.media.j3d.TransformGroup; + + +/** + * 地面などの(基本的に動かない)構造物を表すオブジェクト + * @author 新田直也 + * + */ +public class Ground implements Placeable { + private BaseObject3D groundObj = null; + private BoundingSurface boundingSurface = null; // 衝突判定用ボリュームのキャッシュ + + public Ground(BaseObject3D obj) { + groundObj = obj; + } + + public BaseObject3D getBody() { + return groundObj; + } + + public void updateBody(BaseObject3D obj) { + groundObj = obj; + boundingSurface = null; + } + + @Override + public TransformGroup getTransformGroupToPlace() { + return groundObj.getTransformGroupToPlace(); + } + + /** + * 衝突判定用のボリュームを取得する + * @return 衝突判定用ボリューム(階層化されている場合がある) + */ + BoundingSurface getBoundingSurface() { + if (boundingSurface == null) { + // キャッシュに何も積まれていない場合のみ計算する + BoundingSurfaceVisitor surfaceVisitor = new BoundingSurfaceVisitor(); + if (groundObj instanceof Object3D) { + // Object3Dの場合階層構造をたどる + ((Object3D)groundObj).accept(surfaceVisitor); + } else { + // BaseObject3dの場合階層構造がない + surfaceVisitor.baseVisit(groundObj); + } + boundingSurface = surfaceVisitor.getBoundingSurface(); + } + return boundingSurface; + } +} diff --git a/src/main/java/framework/physics/Inertia3D.java b/src/main/java/framework/physics/Inertia3D.java new file mode 100644 index 0000000..7b9355c --- /dev/null +++ b/src/main/java/framework/physics/Inertia3D.java @@ -0,0 +1,55 @@ +package framework.physics; + +import framework.model3D.OBB; + +import javax.vecmath.Vector3d; + + +public class Inertia3D { + double ixx; + double iyy; + double izz; + static final Inertia3D ZERO = new Inertia3D( 0.0, 0.0, 0.0); + + public Inertia3D(double x,double y,double z){ + this.ixx = x; + this.iyy = y; + this.izz = z; + } + + public Inertia3D(Vector3d v) { + this.ixx = v.x; + this.iyy = v.y; + this.izz = v.z; + } + + public Inertia3D(Solid3D obj) { + BoundingBoxVisitor visitor = new BoundingBoxVisitor(); + obj.accept(visitor); + if (visitor.getObbList().size() == 1) { + OBB obb = visitor.getObbList().get(0); + Vector3d vx = new Vector3d(); + vx.sub(obb.getVertex(4), obb.getVertex(0)); + + Vector3d vy = new Vector3d(); + vy.sub(obb.getVertex(1), obb.getVertex(0)); + + Vector3d vz = new Vector3d(); + vz.sub(obb.getVertex(2), obb.getVertex(0)); + +// System.out.println("vx:" + vx + ",vy:" + vy + ",vz:" + vz); + this.ixx = obj.mass * (1.0/12.0 * (Math.pow(vy.length(), 2.0) + Math.pow(vz.length(), 2.0))); + this.iyy = obj.mass * (1.0/12.0 * (Math.pow(vx.length(), 2.0) + Math.pow(vz.length(), 2.0))); + this.izz = obj.mass * (1.0/12.0 * (Math.pow(vx.length(), 2.0) + Math.pow(vy.length(), 2.0))); + } else { + this.ixx = obj.mass; + this.iyy = obj.mass; + this.izz = obj.mass; + } + } + + public Vector3d getVector3d() { + return new Vector3d(ixx, iyy, izz); + } +} + diff --git a/src/main/java/framework/physics/PhysicalSystem.java b/src/main/java/framework/physics/PhysicalSystem.java new file mode 100644 index 0000000..ef4e755 --- /dev/null +++ b/src/main/java/framework/physics/PhysicalSystem.java @@ -0,0 +1,152 @@ +package framework.physics; + +import framework.model3D.CollisionResult; +import framework.model3D.Position3D; + +import javax.vecmath.Vector3d; +import java.util.ArrayList; + + +public class PhysicalSystem { + + public ArrayList objects = new ArrayList(); + + // 物体の挿入 + public int add(Solid3D s) { + objects.add(s); + return objects.size() - 1; + } + + // 物体の運動 + public void motion(int id, long interval, Force3D f, Position3D appPoint, Ground ground) { + ArrayList forces[] = new ArrayList[objects.size()]; + ArrayList appPoints[] = new ArrayList[objects.size()]; + for (int i = 0; i < objects.size(); i++) { + forces[i] = new ArrayList(); + appPoints[i] = new ArrayList(); + } + + // id番目の外力の計算 + forces[id].add(f); + appPoints[id].add(appPoint); +// objects.get(id).move(interval, f, appPoint); + + double l; // ばねの伸び + + Force3D penalty = new Force3D(0.0, 0.0, 0.0); // ペナルティ法によるペナルティの作用の力 + Force3D inversepenalty; // + // //ペナルティ法によるペナルティの反作用の力 + CollisionResult cr; + Solid3D s; + for (int n = 0; n < objects.size(); n++) { + // 重力の計算 + s = objects.get(n); + forces[n].add(PhysicsUtility.getGravity(s)); + appPoints[n].add(s.getGravityCenter()); +// objects.get(n).move(interval, +// PhysicsFacade.getGravity(objects.get(n)), +// objects.get(n).getGravityCenter()); // 重力の計算 + // 地面との当たり判定 + cr = PhysicsUtility.doesIntersect(s, ground); + // 地面に物体がめり込んでいる場合 + if (cr != null) { + double gk = 5000.0; // 地面でのばね係数 + double e = 1.0; // 地面での跳ね返り時の抵抗係数 + double b = 300.0; + l = cr.length; + // <作用の力の計算> + // ペナルティの変数 +// Vector3d v = cr.normal; +// v.scale(gk * l); + // 作用点ベクトルの作成 + Vector3d r = cr.collisionPoint.getVector3d(); + // (作用点-重心)ベクトル + r.sub(s.getGravityCenter().getVector3d()); + // 角速度ベクトルの作成 + Vector3d angVel = s.getAngularVelocity().getVector3d(); + // 角速度ベクトルと(作用点-重心)ベクトルの外積計算 + angVel.cross(angVel, r); + // 速度ベクトル+角速度ベクトルと(作用点-重心)ベクトルの外積計算 + Vector3d relV = s.getVelocity().getVector3d(); + // 相対速度ベクトルの作成 + relV.add(angVel); + Vector3d v = cr.normal; +//System.out.println(r + "," + (gk * l) + "," + (- relV.dot(v) * b)); + // ペナルティの大きさ決定 + v.scale(gk * l - relV.dot(v) * b); + penalty = new Force3D(v); + + // 作用の力による運動 + forces[n].add(penalty); + appPoints[n].add(cr.collisionPoint); +// objects.get(n).move(interval, penalty, cr.collisionPoint); + } + // 地面に物体がめり込んでいない場合 + else { + } + for (int m = 0; m < n; m++) { + Solid3D s1 = objects.get(n); + Solid3D s2 = objects.get(m); + cr = PhysicsUtility.checkCollision(s1, null, s2, null); + // 物体がめり込んでいる場合 + if (cr != null) { + double sk = 5000; // 物体でのばね係数 + double e = 0.2; // 物体での跳ね返り時の抵抗係数 + double b = 300.0; + l = cr.length; + // <作用の力の計算> + // 作用点ベクトルの作成 + // s1に関する計算 + // s1の角速度ベクトルの作成 + Vector3d r = cr.collisionPoint.getVector3d(); + r.sub(s1.getGravityCenter().getVector3d()); + Vector3d s1AngVel = s1.getAngularVelocity().getVector3d(); + s1AngVel.cross(s1AngVel, r); + // s1の速度ベクトルの作成 + Vector3d s1RelV = s1.getVelocity().getVector3d(); + // s1の速度ベクトル+s1の角速度ベクトルと(作用点-s1の重心)ベクトルの外積計算 + s1RelV.add(s1AngVel); + // s2に関する計算 + // s2の角速度ベクトルの作成 + r = cr.collisionPoint.getVector3d(); + r.sub(s2.getGravityCenter().getVector3d()); + Vector3d s2AngVel = s2.getAngularVelocity().getVector3d(); + s2AngVel.cross(s2AngVel, r); + // s2の速度ベクトルの作成 + Vector3d s2RelV = s2.getVelocity().getVector3d(); + // s2の速度ベクトル+s2の角速度ベクトルと(作用点-s2の重心)ベクトルの外積計算 + s2RelV.add(s2AngVel); + // 相対速度ベクトルの作成 + s1RelV.sub(s2RelV); + // ペナルティの大きさ決定 + Vector3d v = (Vector3d)cr.normal.clone(); +//System.out.println(r + "," + (sk * l) + "," + (- relV.dot(v) * b)); + v.scale(sk * l - s1RelV.dot(v) * b); + penalty = new Force3D(v); + + // 反作用の力の計算 + v.scale(-1); + inversepenalty = new Force3D(v); + + // 作用の力による物体の移動 + forces[n].add(penalty); + appPoints[n].add(cr.collisionPoint); +// s1.move(interval, penalty, cr.collisionPoint); + + // 反作用の力による物体の移動 + forces[m].add(inversepenalty); + appPoints[m].add(cr.collisionPoint); +// s2.move(interval, inversepenalty, cr.collisionPoint); + } +// // 物体がめり込んでいない場合 +// else { +// s2.move(interval, f, s2.getGravityCenter()); +// s1.move(interval, f, s1.getGravityCenter()); +// } + } + } + for (int n2 = 0; n2 < objects.size(); n2++) { + objects.get(n2).move(interval, forces[n2], appPoints[n2]); + } + } +} diff --git a/src/main/java/framework/physics/PhysicsUtility.java b/src/main/java/framework/physics/PhysicsUtility.java new file mode 100644 index 0000000..df2bbf5 --- /dev/null +++ b/src/main/java/framework/physics/PhysicsUtility.java @@ -0,0 +1,191 @@ +package framework.physics; + +import framework.model3D.BoundingSurface; +import framework.model3D.CollisionResult; +import framework.model3D.Object3D; +import framework.model3D.Position3D; + +import javax.media.j3d.BoundingSphere; +import javax.media.j3d.Transform3D; +import javax.vecmath.Vector3d; +import java.util.ArrayList; + + +/** + * 物理演算のコア + * @author 新田直也 + * + */ +public class PhysicsUtility { + public static final double GRAVITY = 9.8; // 重力の加速度 + public static final Vector3d horizon = new Vector3d(1.0, 0.0, 0.0); + public static final Vector3d vertical = new Vector3d(0.0, 1.0, 0.0); + + public static Vector3d gravityDirection = new Vector3d(0.0, 1.0, 0.0); + + /** + * 物体に加わる重力を求める + * @param body 対象物体 + * @return bodyに加わる重力 + */ + public static Force3D getGravity(Solid3D body) { + return new Force3D(gravityDirection.x * -body.mass * GRAVITY, gravityDirection.y * -body.mass * GRAVITY, gravityDirection.z * -body.mass * GRAVITY); + } + + /** + * @param v + * :単位ベクトル、あるいはゼロベクトルを引数で渡すこと + */ + public static void setGravityDirection(Vector3d v) { + gravityDirection = new Vector3d(v.x, v.y, v.z); + } + + // モーメントの計算 + static Vector3d calcMoment(Force3D f, Position3D gravityCenter, + Position3D applicationPoint) { + Vector3d v1 = applicationPoint.getVector3d(); + Vector3d v2 = gravityCenter.getVector3d(); + v1.sub(v2); + + Vector3d cv = new Vector3d(); + Vector3d fv = f.getVector3d(); + cv.cross(v1, fv); + return cv; + + } + + /** + * 物体の反発係数から衝突時に加わる力を求める + * @param interval 衝突している時間 + * @param nor 物体がぶつかった面の宝仙ベクトル + * @param solid 物体 + * @return 衝突時に加わる力 + */ + public static Force3D calcForce(long interval, Vector3d nor, Solid3D solid) { + double f1 = 0.0; + Vector3d vf = new Vector3d(solid.getVelocity().getX(), solid + .getVelocity().getY(), solid.getVelocity().getZ()); + f1 = solid.mass * (vf.length() + solid.e * vf.length()) + / ((double) interval / 1000.0); + nor.scale(f1); + Force3D f = new Force3D(nor.x, nor.y, nor.z); + return f; + } + + /** + * 物体と地面との衝突判定 + * @param obj 物体 + * @param ground 地面 + * @return 衝突情報(nullのとき衝突なし) + */ + public static CollisionResult doesIntersect(Solid3D obj, Ground ground) { + if (ground == null) return null; + CollisionResult cr = null; + BoundingSurface boundingSurface = ground.getBoundingSurface(); + + // BoundingSphereを使って大雑把に衝突判定を行う + ArrayList boundingSurfaceList = null; + if (obj.bs != null) { + BoundingSphere bs = (BoundingSphere) (obj.bs.clone()); + Transform3D t3d = new Transform3D(); + obj.center.getTransform(t3d); + bs.transform(t3d); + obj.scale.getTransform(t3d); + bs.transform(t3d); + obj.rot.getTransform(t3d); + bs.transform(t3d); + obj.pos.getTransform(t3d); + bs.transform(t3d); + // 粗い衝突判定を行う(最上位のBoundingSurfaceとBoundingSphereの間で) + boundingSurfaceList = boundingSurface.intersect(bs); + bs = null; + t3d = null; + } + + if (obj.bs == null) { + // BoundingSphere がまだ作られていななかった場合、 + // 詳細な衝突判定のために、最上位の全 BoundingSurface を取得する + boundingSurfaceList.add(boundingSurface); + } + + if (boundingSurfaceList.size() > 0) { + // 粗い衝突判定で衝突していた場合、OBBの集合を用いてより詳しい衝突判定を行う + // (BoundingSphere がまだ作られていない場合、OBB の作成と同時に BoundingSphere を作成される) + BoundingBoxVisitor obbVisitor = new BoundingBoxVisitor(); + obj.accept(obbVisitor); + for (int i = 0; i < obbVisitor.getObbList().size(); i++) { + // OBBと衝突判定をする場合は、地面を多角形のまま扱う + for (int j = 0; j < boundingSurfaceList.size(); j++) { + cr = boundingSurfaceList.get(j).intersect(obbVisitor.getObbList().get(i)); + if (cr != null) { + return cr; + } + } + } + obbVisitor = null; + } + return null; + } + + /** + * 物体同士の衝突判定 + * @param obj1 物体1 + * @param part1 判定する物体1の部分の名称 + * @param obj2 物体2 + * @param part2 判定する物体2の部分の名称 + * @return 衝突情報(nullのとき衝突なし) + */ + public static CollisionResult checkCollision(Object3D obj1, String part1, + Object3D obj2, String part2) { + CollisionResult cr = null; + + // BoundingSphereを使って大雑把に衝突判定を行う + boolean f = false; + if (obj1.bs != null && obj2.bs != null) { + // sol1 の BoundingSphere を計算 + BoundingSphere bs1 = (BoundingSphere) (obj1.bs.clone()); + Transform3D t3d = new Transform3D(); + obj1.center.getTransform(t3d); + bs1.transform(t3d); + obj1.scale.getTransform(t3d); + bs1.transform(t3d); + obj1.rot.getTransform(t3d); + bs1.transform(t3d); + obj1.pos.getTransform(t3d); + bs1.transform(t3d); + + // sol2 の BoundingSphere を計算 + BoundingSphere bs2 = (BoundingSphere) (obj2.bs.clone()); + obj2.center.getTransform(t3d); + bs2.transform(t3d); + obj2.scale.getTransform(t3d); + bs2.transform(t3d); + obj2.rot.getTransform(t3d); + bs2.transform(t3d); + obj2.pos.getTransform(t3d); + bs2.transform(t3d); + + // BoundingSphere 同士の衝突判定 + if (bs1.intersect(bs2)) f = true; + t3d = null; + bs1 = null; + bs2 = null; + } + if (f || obj1.bs == null || obj2.bs == null) { + BoundingBoxVisitor visitor1 = new BoundingBoxVisitor(part1); + BoundingBoxVisitor visitor2 = new BoundingBoxVisitor(part2); + obj1.accept(visitor1); + obj2.accept(visitor2); + int i, j; + for (i = 0; i < visitor1.getObbList().size(); i++) { + for (j = 0; j < visitor2.getObbList().size(); j++) { + cr = visitor2.getObbList().get(j).intersect(visitor1.getObbList().get(i)); + if (cr != null) { + return cr; + } + } + } + } + return null; + } +} diff --git a/src/main/java/framework/physics/Solid3D.java b/src/main/java/framework/physics/Solid3D.java new file mode 100644 index 0000000..85cc700 --- /dev/null +++ b/src/main/java/framework/physics/Solid3D.java @@ -0,0 +1,170 @@ +package framework.physics; + +import framework.model3D.Object3D; +import framework.model3D.Position3D; +import framework.model3D.Quaternion3D; + +import javax.vecmath.AxisAngle4d; +import javax.vecmath.Vector3d; +import java.util.ArrayList; + +/** + * 物理的な振る舞いをする物体(剛体)を表す + * @author 新田直也 + * + */ +public class Solid3D extends Object3D { + private Velocity3D velocity; + private AngularVelocity3D angularvelocity; + private Position3D gravityCenter = getPosition3D(); + public double e = 1.0; + public double mass = 10; + private Inertia3D inertia = null; + + // コピーコンストラクタ + public Solid3D(Object3D obj) { + super(obj); + velocity = new Velocity3D(); + quaternion = new Quaternion3D(); + angularvelocity = new AngularVelocity3D(); + inertia = new Inertia3D(this); + } + + public Solid3D(Object3D obj, double mass) { + super(obj); + velocity = new Velocity3D(); + quaternion = new Quaternion3D(); + angularvelocity = new AngularVelocity3D(); + this.mass = mass; + inertia = new Inertia3D(this); + } + + public Solid3D(Solid3D solid) { + super(solid); + velocity = new Velocity3D(solid.velocity); + quaternion = new Quaternion3D(solid.getQuaternion()); + angularvelocity = new AngularVelocity3D(solid.angularvelocity); + mass = solid.mass; + inertia = new Inertia3D(this); + } + + /** + * 力学運動の計算(加わる力が1つの場合) + * @param interval 単位時間 + * @param f 力 + * @param applicationPoint 力の作用点 + */ + public void move(long interval, Force3D f, Position3D applicationPoint) { + // モーメントの計算 + Vector3d moment = PhysicsUtility.calcMoment(f, getGravityCenter(), + applicationPoint); + moveSub(interval, f, moment); + } + + /** + * 力学運動の計算(同時に複数の力が加わる場合) + * @param interval 単位時間 + * @param forces 力(複数) + * @param appPoints それぞれの力の作用点 + */ + public void move(long interval, ArrayList forces, + ArrayList appPoints) { + // 重心に加わる力の合計を求める + Force3D f = new Force3D(0.0, 0.0, 0.0); + for (int n = 0; n < forces.size(); n++) { + f.add(forces.get(n)); + } + + // モーメントの合計を計算する + Position3D gc = getGravityCenter(); + Vector3d moment = new Vector3d(0.0, 0.0, 0.0); + for (int n2 = 0; n2 < forces.size(); n2++) { + moment.add(PhysicsUtility.calcMoment(forces.get(n2), gc, appPoints.get(n2))); + } + moveSub(interval, f, moment); + } + + private void moveSub(long interval, Force3D f, Vector3d moment) { + // 1.重心の運動方程式(ニュートン方程式) + // 加速度、速度計算 + Vector3d deltaV = f.getVector3d(); // 力ベクトルの取得 + deltaV.scale(1.0 / mass * ((double) interval / 1000.0)); // 加速度から速度の差分を計算 + Velocity3D v = getVelocity().add(deltaV); // 速度に差分を加算 + apply(v, false); + + // 重心位置計算 + Vector3d deltaP = velocity.getVector3d(); // 速度ベクトルの取得 + deltaP.scale(((double) interval / 1000.0)); + Position3D p = getPosition3D().add(deltaP); // 位置に差分を加算 + apply(p, false); + + // 2.オイラーの角運動方程式 + + // 角加速度、角速度計算 + AngularVelocity3D w = getAngularVelocity(); + Vector3d deltaAngularV = new Vector3d( + (moment.x + (inertia.iyy - inertia.izz) * w.getY() * w.getZ()) / inertia.ixx, + (moment.y + (inertia.izz - inertia.ixx) * w.getZ() * w.getX()) / inertia.iyy, + (moment.z + (inertia.ixx - inertia.iyy) * w.getX() * w.getY()) / inertia.izz); + deltaAngularV.scale((double) interval / 1000.0); + w.add(deltaAngularV); + apply(w, false); + + // 角速度による回転計算 + AxisAngle4d axisAngle = w.getAxisAngle4d(); + axisAngle.angle *= ((double) interval / 1000.0); + Quaternion3D q = getQuaternion().add(axisAngle); + apply(q, false); + } + + // 複製を作る + public Object3D duplicate() { + Object3D copy = new Solid3D(this); + return copy; + } + + public void scale(double s) { + super.scale(s); + inertia = new Inertia3D(this); + } + + public void scale(double sx, double sy, double sz) { + super.scale(sx, sy, sz); + inertia = new Inertia3D(this); + } + + public Velocity3D getVelocity() { + return (Velocity3D) velocity.clone(); + } + + public AngularVelocity3D getAngularVelocity() { + return (AngularVelocity3D) angularvelocity.clone(); + } + + // Velocity3D の applyTo 以外からは呼ばないこと + void setVelocity(Velocity3D v) { + velocity = (Velocity3D) v.clone(); + } + + // AngularVelocity3D の applyTo 以外からは呼ばないこと + void setAngularVelocity(AngularVelocity3D w) { + angularvelocity = (AngularVelocity3D) w.clone(); + } + + public void setGravityCenter(Position3D gravityCenter) { + this.gravityCenter = gravityCenter; + } + + public Position3D getGravityCenter() { + return getPosition3D().add(gravityCenter); + } + + public void setMass(double mass) { + this.mass = mass; + } + + public double getMass() { + return mass; + } + +} diff --git a/src/main/java/framework/physics/Velocity3D.java b/src/main/java/framework/physics/Velocity3D.java new file mode 100644 index 0000000..bcec8bb --- /dev/null +++ b/src/main/java/framework/physics/Velocity3D.java @@ -0,0 +1,149 @@ +package framework.physics; + +import framework.model3D.Object3D; +import framework.model3D.Property3D; + +import javax.media.j3d.Transform3D; +import javax.vecmath.Vector3d; + + +public class Velocity3D extends Property3D { + private double x; + private double y; + private double z; + + public Velocity3D(Velocity3D v) { + x = v.x; + y = v.y; + z = v.z; + } + + public Velocity3D(Vector3d v) { + this.x = v.x; + this.y = v.y; + this.z = v.z; + } + + public Velocity3D(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Velocity3D() { + x = 0.0; + y = 0.0; + z = 0.0; + } + + public void applyTo(Object3D o) { + ((Solid3D)o).setVelocity(this); + } + + public double getX() { + // TODO Auto-generated method stub + return this.x; + } + + public double getY() { + // TODO Auto-generated method stub + return this.y; + } + + public double getZ() { + // TODO Auto-generated method stub + return this.z; + } + + public Velocity3D setX(double x) { + // TODO Auto-generated method stub + this.x = x; + return this; + } + + public Velocity3D setY(double y) { + // TODO Auto-generated method stub + this.y = y; + return this; + } + + public Velocity3D setZ(double z) { + // TODO Auto-generated method stub + this.z = z; + return this; + } + + public Velocity3D add(double x, double y, double z){ + this.x += x; + this.y += y; + this.z += z; + return this; + } + + public Velocity3D add(Vector3d v) { + // TODO Auto-generated method stub + this.x += v.x; + this.y += v.y; + this.z += v.z; + return this; + } + + public Velocity3D setVector3d(Vector3d v) { + x = v.x; + y = v.y; + z = v.z; + return this; + } + + public Vector3d getVector3d() { + return new Vector3d(x,y,z); + } + + @Override + public Property3D clone() { + // TODO Auto-generated method stub + return new Velocity3D(this); + } + + public Velocity3D rotX(double a) { + Vector3d v = getVector3d(); + Transform3D rotX = new Transform3D(); + rotX.rotX(a); + rotX.transform(v); + setVector3d(v); + return this; + } + + public Velocity3D rotY(double a) { + Vector3d v = getVector3d(); + Transform3D rotY = new Transform3D(); + rotY.rotY(a); + rotY.transform(v); + setVector3d(v); + return this; + } + + public Velocity3D rotZ(double a) { + Vector3d v = getVector3d(); + Transform3D rotZ = new Transform3D(); + rotZ.rotZ(a); + rotZ.transform(v); + setVector3d(v); + return this; + } + + public Velocity3D setVelocity(double velocity) { + Vector3d v = getVector3d(); + double oldV = v.length(); + v.scale(velocity / oldV); + setVector3d(v); + return this; + } + + public Velocity3D mul(double d) { + this.x *= d; + this.y *= d; + this.z *= d; + return this; + } +} diff --git a/src/main/java/framework/scenario/Event.java b/src/main/java/framework/scenario/Event.java new file mode 100644 index 0000000..a6ecd7f --- /dev/null +++ b/src/main/java/framework/scenario/Event.java @@ -0,0 +1,13 @@ +package framework.scenario; + +public class Event { + private String name; + + public Event(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/framework/scenario/FSM.java b/src/main/java/framework/scenario/FSM.java new file mode 100644 index 0000000..d2dce6c --- /dev/null +++ b/src/main/java/framework/scenario/FSM.java @@ -0,0 +1,38 @@ +package framework.scenario; + +import java.util.Collection; +import java.util.Hashtable; +import java.util.Iterator; + +public class FSM { + private State initialState = null; + protected State currentState = null; + private Hashtable states = new Hashtable(); + + public FSM(State initialState, Hashtable states) { + this.initialState = initialState; + this.states = states; + currentState = initialState; + Collection allStates = states.values(); + Iterator it = allStates.iterator(); + while (it.hasNext()) { + State s = it.next(); + s.setOwner(this); + } + } + + public void addState(String stateName, State s) { + states.put(stateName, s); + s.setOwner(this); + } + + public boolean trans(Event e) { + currentState = currentState.getSuccessor(e); + if (currentState == null) return false; + return true; + } + + public State getCurrentState() { + return currentState; + } +} diff --git a/src/main/java/framework/scenario/IWorld.java b/src/main/java/framework/scenario/IWorld.java new file mode 100644 index 0000000..d1e6b92 --- /dev/null +++ b/src/main/java/framework/scenario/IWorld.java @@ -0,0 +1,10 @@ +package framework.scenario; + +public interface IWorld { + abstract public void dialogOpen(); + abstract public void dialogClose(); + abstract public void dialogMessage(String message); + abstract public void showOption(int n, String option); + abstract public boolean isDialogOpen(); + abstract public void action(String action, Event event, ScenarioState nextState); +} diff --git a/src/main/java/framework/scenario/ScenarioAction.java b/src/main/java/framework/scenario/ScenarioAction.java new file mode 100644 index 0000000..a4718a0 --- /dev/null +++ b/src/main/java/framework/scenario/ScenarioAction.java @@ -0,0 +1,13 @@ +package framework.scenario; + +public class ScenarioAction { + private String name; + + public ScenarioAction(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/framework/scenario/ScenarioFSM.java b/src/main/java/framework/scenario/ScenarioFSM.java new file mode 100644 index 0000000..1a88956 --- /dev/null +++ b/src/main/java/framework/scenario/ScenarioFSM.java @@ -0,0 +1,21 @@ +package framework.scenario; + +import java.util.Hashtable; + +public class ScenarioFSM extends FSM { + ScenarioManager manager; + + public ScenarioFSM(State initialState, Hashtable states, ScenarioManager manager) { + super(initialState, states); + this.manager = manager; + } + + public boolean trans(Event e) { + e = ((ScenarioState)currentState).canTrans(e); + if (e == null) return false; + ScenarioAction action = ((ScenarioState)currentState).getAction(e); + boolean result = super.trans(e); + manager.action(action, e, (ScenarioState)currentState); + return result; + } +} diff --git a/src/main/java/framework/scenario/ScenarioManager.java b/src/main/java/framework/scenario/ScenarioManager.java new file mode 100644 index 0000000..e8f94b1 --- /dev/null +++ b/src/main/java/framework/scenario/ScenarioManager.java @@ -0,0 +1,177 @@ +package framework.scenario; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.util.*; +import java.util.Map.Entry; + +public class ScenarioManager { + private IWorld realWorld; + private Hashtable stateMachines = new Hashtable(); + private Hashtable allStates = new Hashtable(); + private Hashtable allEvents = new Hashtable(); + + public ScenarioManager(String xmlFileName, IWorld realWorld) { + this.realWorld = realWorld; + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + + factory.setValidating(true); + factory.setAttribute( + "http://java.sun.com/xml/jaxp/properties/schemaLanguage", + "http://www.w3.org/2001/XMLSchema"); + + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setErrorHandler(new DefaultHandler()); + Document document = builder.parse(xmlFileName); + + // 有限状態マシンの作成、状態の登録、イベントの登録 + Hashtable allTrans = new Hashtable(); + NodeList scenario = document.getChildNodes(); + NodeList fsmNodes = scenario.item(0).getChildNodes(); + for (int n = 0; n < fsmNodes.getLength(); n++) { + Node fsmNode = fsmNodes.item(n); + if (fsmNode.getNodeName().equals("FSM")) { + Hashtable fsmStates = new Hashtable(); + NodeList stateNodes = fsmNode.getChildNodes(); + for (int m = 0; m < stateNodes.getLength(); m++) { + Node stateNode = stateNodes.item(m); + if (stateNode.getNodeName().equals("State")) { + ScenarioState state = new ScenarioState(); + String stateName = stateNode.getAttributes().getNamedItem("name").getNodeValue(); + fsmStates.put(stateName, state); + allStates.put(stateName, state); + Node stateMessageNode = stateNode.getAttributes().getNamedItem("message"); + if (stateMessageNode != null) state.setMessage(stateMessageNode.getNodeValue()); + NodeList eventNodes = stateNode.getChildNodes(); + allTrans.put(state, eventNodes); + for (int l = 0; l < eventNodes.getLength(); l++) { + Node eventNode = eventNodes.item(l); + if (eventNode.getNodeName().equals("Event")) { + String eventName = eventNode.getAttributes().getNamedItem("name").getNodeValue(); + Event e = allEvents.get(eventName); + if (e == null) { + e = new Event(eventName); + allEvents.put(eventName, e); + } + } + } + } + } + String initialStateName = fsmNode.getAttributes().getNamedItem("initial").getNodeValue(); + State initialState = fsmStates.get(initialStateName); + ScenarioFSM fsm = new ScenarioFSM(initialState, fsmStates, this); + String fsmName = fsmNode.getAttributes().getNamedItem("name").getNodeValue(); + stateMachines.put(fsmName, fsm); + } + } + + // 状態遷移を設定する + Set> allTransEntries = allTrans.entrySet(); + Iterator> it = allTransEntries.iterator(); + while (it.hasNext()) { + Entry transEntry = it.next(); + ScenarioState state = transEntry.getKey(); + NodeList eventNodes = transEntry.getValue(); + for (int l = 0; l < eventNodes.getLength(); l++) { + Node eventNode = eventNodes.item(l); + if (eventNode.getNodeName().equals("Event")) { + Node eventNameNode = eventNode.getAttributes().getNamedItem("name"); + Event e = null; + if (eventNameNode != null) { + String eventName = eventNameNode.getNodeValue(); + e = allEvents.get(eventName); + } + Node nextStateNameNode = eventNode.getAttributes().getNamedItem("trans"); + State nextState = null; + if (nextStateNameNode != null) { + String nextStateName = nextStateNameNode.getNodeValue(); + nextState = allStates.get(nextStateName); + } + Node syncEventNameNode = eventNode.getAttributes().getNamedItem("sync"); + Event syncEvent = null; + if (syncEventNameNode != null) { + String syncEventName = syncEventNameNode.getNodeValue(); + syncEvent = allEvents.get(syncEventName); + } + ArrayList guards = new ArrayList(); + Node guardStateNameNode = eventNode.getAttributes().getNamedItem("guard"); + if (guardStateNameNode != null) { + String guardStateName = guardStateNameNode.getNodeValue(); + State guardState = allStates.get(guardStateName); + if (guardState != null) guards.add(guardState); + } + Node guardStateNameNode2 = eventNode.getAttributes().getNamedItem("guard2"); + if (guardStateNameNode2 != null) { + String guardStateName2 = guardStateNameNode2.getNodeValue(); + State guardState2 = allStates.get(guardStateName2); + if (guardState2 != null) guards.add(guardState2); + } + Node guardStateNameNode3 = eventNode.getAttributes().getNamedItem("guard3"); + if (guardStateNameNode3 != null) { + String guardStateName3 = guardStateNameNode3.getNodeValue(); + State guardState3 = allStates.get(guardStateName3); + if (guardState3 != null) guards.add(guardState3); + } + Node actionNameNode = eventNode.getAttributes().getNamedItem("action"); + ScenarioAction action = null; + if (actionNameNode != null) { + String actionName = actionNameNode.getNodeValue(); + action = new ScenarioAction(actionName); + } + state.addTransition(e, nextState, syncEvent, guards, action); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void fire(Event e){ + Collection fsms = stateMachines.values(); + Iterator it = fsms.iterator(); + while (it.hasNext()) { + ScenarioFSM fsm = it.next(); + fsm.trans(e); + } + } + + public void fire(String eventName) { + Event e = allEvents.get(eventName); + if (e == null) return; + fire(e); + } + + public void action(ScenarioAction action, Event event, ScenarioState nextState) { + if (action != null) { + String sAction = action.getName(); + if (sAction.equals("openDialog")) { + realWorld.dialogOpen(); + } else if (sAction.equals("closeDialog")) { + realWorld.dialogClose(); + } else if (sAction.equals("print")) { + System.out.println(event.getName()); + } + realWorld.action(sAction, event, nextState); + } + if (realWorld.isDialogOpen()) { + String message = nextState.getMessage(); + if (message != null) { + realWorld.dialogMessage(message); + Enumeration events = nextState.getEvents(); + int n = 0; + while (events.hasMoreElements()) { + Event ev = events.nextElement(); + realWorld.showOption(n, ev.getName()); + n++; + } + } + } + } +} diff --git a/src/main/java/framework/scenario/ScenarioState.java b/src/main/java/framework/scenario/ScenarioState.java new file mode 100644 index 0000000..3b1f7f1 --- /dev/null +++ b/src/main/java/framework/scenario/ScenarioState.java @@ -0,0 +1,67 @@ +package framework.scenario; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Hashtable; + +public class ScenarioState extends State { + private Hashtable transitionSyncs = new Hashtable(); + private Hashtable> transitionGuards = new Hashtable>(); + private Hashtable transitionActions = new Hashtable(); + private String message = null; + + public ScenarioState() { + super(); + } + + public ScenarioState(Hashtable transitions, + Hashtable transitionSyncs, + Hashtable> transitionGuards, + Hashtable transitionActions, + String message) { + super(transitions); + this.transitionSyncs = transitionSyncs; + this.transitionGuards = transitionGuards; + this.transitionActions = transitionActions; + this.message = message; + } + + public void addTransition(Event event, State succ, Event syncEvent, ArrayList guards, ScenarioAction action) { + addTransition(event, succ); + if (syncEvent != null) transitionSyncs.put(event, syncEvent); + if (guards != null) transitionGuards.put(event, guards); + if (action != null) transitionActions.put(event, action); + } + + Event canTrans(Event event) { + Enumeration events = getEvents(); + while (events.hasMoreElements()) { + Event e = events.nextElement(); + if (event == e || event == transitionSyncs.get(e)) { + ArrayList guardStates = transitionGuards.get(e); + boolean bSatisfyGuard = true; + for (int n = 0; n < guardStates.size(); n++) { + State s = guardStates.get(n); + if (s.getOwner().getCurrentState() != s) { + bSatisfyGuard = false; + break; + } + } + if (bSatisfyGuard) return e; + } + } + return null; + } + + public ScenarioAction getAction(Event e) { + return transitionActions.get(e); + } + + public void setMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/framework/scenario/State.java b/src/main/java/framework/scenario/State.java new file mode 100644 index 0000000..7be83fe --- /dev/null +++ b/src/main/java/framework/scenario/State.java @@ -0,0 +1,37 @@ +package framework.scenario; + +import java.util.Enumeration; +import java.util.Hashtable; + +public class State { + private FSM owner = null; + private Hashtable transitions = new Hashtable(); + + public State() { + } + + public State(Hashtable transitions) { + this.transitions = transitions; + } + + public void addTransition(Event e, State s) { + transitions.put(e, s); + } + + public Enumeration getEvents() { + return transitions.keys(); + } + + public State getSuccessor(Event e) { + if (!transitions.containsKey(e)) return null; + return transitions.get(e); + } + + public void setOwner(FSM fsm) { + owner = fsm; + } + + public FSM getOwner() { + return owner; + } +} diff --git a/src/main/java/framework/schedule/ScheduleManager.java b/src/main/java/framework/schedule/ScheduleManager.java new file mode 100644 index 0000000..fd5a67d --- /dev/null +++ b/src/main/java/framework/schedule/ScheduleManager.java @@ -0,0 +1,28 @@ +package framework.schedule; + +import java.util.Hashtable; + +public class ScheduleManager { + private static ScheduleManager theInstance = null; + private Hashtable taskControllerTable = new Hashtable(); + + private ScheduleManager() { + } + + public static ScheduleManager getInstance() { + if (theInstance == null) { + theInstance = new ScheduleManager(); + } + return theInstance; + } + + public TaskController registerTask(String taskName) { + TaskController controller = new TaskController(); + taskControllerTable.put(taskName, controller); + return controller; + } + + public TaskController getController(String taskName) { + return taskControllerTable.get(taskName); + } +} diff --git a/src/main/java/framework/schedule/TaskController.java b/src/main/java/framework/schedule/TaskController.java new file mode 100644 index 0000000..942720c --- /dev/null +++ b/src/main/java/framework/schedule/TaskController.java @@ -0,0 +1,37 @@ +package framework.schedule; + +public class TaskController { + private boolean bActive = false; + + synchronized public boolean activate() { + if (isActive()) return false; + setActive(true); + return true; + } + synchronized public void deactivate() { + synchronized(this) { + notify(); + } + setActive(false); + } + + synchronized public void waitForActivation() { + if (!isActive()) return; + try { + synchronized(this) { + wait(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + setActive(false); // 不要かもしれない + } + + private boolean isActive() { + return bActive; + } + + private void setActive(boolean flag) { + bActive = flag; + } +} diff --git a/src/main/java/framework/view3D/Camera3D.java b/src/main/java/framework/view3D/Camera3D.java new file mode 100644 index 0000000..79d180f --- /dev/null +++ b/src/main/java/framework/view3D/Camera3D.java @@ -0,0 +1,425 @@ +package framework.view3D; + +import framework.model3D.*; + +import javax.media.j3d.*; +import javax.vecmath.SingularMatrixException; +import javax.vecmath.Vector3d; +import java.util.ArrayList; + +/** + * 画角調整機能が付いたカメラ
    + * 視点、注視対象、視線のうち2つを指定して使う。 + * @author 新田直也 + * + */ +public class Camera3D { + private static final double NEAREST = 3.0; + private static final Vector3d VIEW_FORWARD = new Vector3d(0.0, 0.0, -1.0); + private static final Vector3d VIEW_DOWN = new Vector3d(0.0, -1.0, 0.0); + private Universe universe = null; + private View view = null; + private TransformGroup viewPlatformTransform = null; + protected ArrayList targetObjList = null; + protected ArrayList targetList = null; + protected Position3D viewPoint = null; + protected Object3D viewPointObj = null; + private Vector3d viewLine = null; + private Vector3d cameraBack = null; + + // 以下の変数は省メモリ化のため導入 + private Transform3D worldToView = new Transform3D(); + private double matrix[] = new double[]{1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0}; + + private Transform3D cameraTransform = new Transform3D(); + + public Camera3D(Universe universe) { + this.universe = universe; + view = new View(); + view.setPhysicalBody(new PhysicalBody()); + view.setPhysicalEnvironment(new PhysicalEnvironment()); + view.setTransparencySortingPolicy(View.TRANSPARENCY_SORT_GEOMETRY); + view.setBackClipDistance(10000.0); + ViewPlatform viewPlatform = new ViewPlatform(); + view.attachViewPlatform(viewPlatform); + viewPlatformTransform = new TransformGroup(); + viewPlatformTransform.setCapability(TransformGroup.ALLOW_TRANSFORM_READ); + viewPlatformTransform.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); + viewPlatformTransform.addChild(viewPlatform); + universe.place(viewPlatformTransform); + } + + public Universe getUniverse() { + return universe; + } + + public View getView() { + return view; + } + + /** + * カメラの注視点を追加する + * + * @param target + * 注視点 + */ + public void addTarget(Position3D target) { + if (targetList == null) targetList = new ArrayList(); + targetList.add(target); + } + + /** + * カメラの注視対象を追加する + * + * @param target + * 注視対象 + */ + public void addTarget(Object3D target) { + if (targetObjList == null) targetObjList = new ArrayList(); + targetObjList.add(target); + } + + /** + * カメラの注視対象を追加する + * + * @param target + * 注視対象 + */ + public void addTarget(Placeable target) { + if (targetObjList == null) targetObjList = new ArrayList(); + if (target.getBody() instanceof Object3D) { + targetObjList.add((Object3D)target.getBody()); + } + } + + /** + * カメラの視点を設定する + * + * @param viewPoint + * 視点 + */ + public void setViewPoint(Position3D viewPoint) { + this.viewPoint = viewPoint; + } + + /** + * カメラの視点を設定する + * + * @param viewPoint + * 視点となるオブジェクト + */ + public void setViewPoint(Object3D viewPointObj) { + this.viewPointObj = viewPointObj; + } + + /** + * カメラの視点を設定する + * + * @param viewPoint + * 視点となる登場物 + */ + public void setViewPoint(Placeable viewPointActor) { + if (viewPointActor.getBody() instanceof Object3D) { + viewPointObj = (Object3D)viewPointActor.getBody(); + } + } + + /** + * 手前からの視線に設定する + */ + public void setSideView() { + viewLine = VIEW_FORWARD; + } + + /** + * 上から見下ろした視線に設定する + */ + public void setTopView() { + viewLine = VIEW_DOWN; + } + + /** + * 視線を設定する + */ + public void setViewLine(Vector3d viewLine) { + this.viewLine = viewLine; + } + + /** + * 注視対象に対してカメラを引く方向と距離を設定する + * @param cameraBack + */ + public void setCameraBack(Vector3d cameraBack) { + this.cameraBack = cameraBack; + } + + public Vector3d getCameraBack() { + return cameraBack; + } + + /** + * 視野角を設定する + * @param a 視野角 + */ + public void setFieldOfView(double a) { + view.setFieldOfView(a); + } + + /** + * 視野角を取得する + * @return 視野角 + */ + public double getFieldOfView() { + return view.getFieldOfView(); + } + + /** + * フロントクリップ距離を設定する + * @param d + */ + public void setFrontClipDistance(double d) { + view.setFrontClipDistance(d); + } + + /** + * バッククリップ距離を設定する + * @param d + */ + public void setBackClipDistance(double d) { + view.setBackClipDistance(d); + } + + /** + * 注視対象や視点の移動、視線の変化に伴う画角調整 + */ + public void adjust(long interval) { + // カメラ座標系(vx, vy, vz)を計算する + Vector3d vx = new Vector3d(), vy = new Vector3d(), vz = new Vector3d(); + if (viewLine == null) { + // 視線が設定されていない場合 + if ((viewPoint == null && viewPointObj == null) + || ((targetObjList == null || targetObjList.size() == 0) + && (targetList == null || targetList.size() == 0))) { + // 視点または注視対象が設定されていない場合、手前からの視線にする + vz.negate(VIEW_FORWARD); + } else { + // 注視対象の重心を注視点とする + Vector3d center = new Vector3d(); + if (targetObjList != null && targetObjList.size() != 0) { + for (int i = 0; i < targetObjList.size(); i++) { + Position3D position = targetObjList.get(i).getPosition3D(); + center.add(position.getVector3d()); + } + center.scale(1.0 / targetObjList.size()); + } else if (targetList != null && targetList.size() != 0) { + for (int i = 0; i < targetList.size(); i++) { + Position3D position = targetList.get(i); + center.add(position.getVector3d()); + } + center.scale(1.0 / targetList.size()); + } + if (viewPoint != null) { + center.sub(viewPoint.getVector3d()); + } else { + center.sub(viewPointObj.getPosition3D().getVector3d()); + } + center.normalize(); + vz.negate(center); + } + } else { + // 視線が設定されている場合 vz を視線方向 と逆向きに設定する + viewLine.normalize(); + vz.negate(viewLine); + } + vx.cross(vz, VIEW_DOWN); + if (vx.length() > GeometryUtility.TOLERANCE) { + vx.normalize(); + } else { + vx = new Vector3d(1.0, 0.0, 0.0); + } + vy.cross(vz, vx); + + // 世界座標からカメラ座標への変換を計算する + if (viewPoint != null || viewPointObj != null) { + // 視点が設定されている場合 + Vector3d vp; + if (viewPoint != null) { + vp = viewPoint.getVector3d(); + } else { + vp = viewPointObj.getPosition3D().getVector3d(); + } + matrix[0] = vx.x; matrix[1] = vx.y; matrix[2] = vx.z; matrix[3] = 0.0; + matrix[4] = vy.x; matrix[5] = vy.y; matrix[6] = vy.z; matrix[7] = 0.0; + matrix[8] = vz.x; matrix[9] = vz.y; matrix[10] = vz.z; matrix[11] = 0.0; + matrix[12] = 0.0; matrix[13] = 0.0; matrix[14] = 0.0; matrix[15] = 1.0; + worldToView.set(matrix); + try { + worldToView.invert(); + } catch (SingularMatrixException e) { + return; + } + worldToView.setTranslation(vp); + } else { + // 視点が設定されていない場合、注視対象と視線から(カメラ座標系上での)視点を逆計算する + if ((targetObjList == null || targetObjList.size() == 0) + && (targetList == null || targetList.size() == 0)) return; // 視点も注視対象も設定されていない + double xmax = 0; + double xmin = 0; + double ymax = 0; + double ymin = 0; + + // 座標の取得 + Vector3d center = new Vector3d(); + if (targetObjList != null && targetObjList.size() != 0) { + for (int i = 0; i < targetObjList.size(); i++) { + Position3D position = targetObjList.get(i).getPosition3D(); + center.add(position.getVector3d()); + double px = position.getVector3d().dot(vx); + double py = position.getVector3d().dot(vy); + if (i == 0) { + xmax = xmin = px; + ymax = ymin = py; + } else { + if (xmax < px) + xmax = px; + if (xmin > px) + xmin = px; + if (ymax < py) + ymax = py; + if (ymin > py) + ymin = py; + } + } + center.scale(1.0 / targetObjList.size()); + } else if (targetList != null && targetList.size() != 0) { + for (int i = 0; i < targetList.size(); i++) { + Position3D position = targetList.get(i); + center.add(position.getVector3d()); + double px = position.getVector3d().dot(vx); + double py = position.getVector3d().dot(vy); + if (i == 0) { + xmax = xmin = px; + ymax = ymin = py; + } else { + if (xmax < px) + xmax = px; + if (xmin > px) + xmin = px; + if (ymax < py) + ymax = py; + if (ymin > py) + ymin = py; + } + } + center.scale(1.0 / targetList.size()); + } + + double x = (xmax + xmin) / 2; + double y = (ymax + ymin) / 2; + double x_diff = Math.abs(xmax - x); + double y_diff = Math.abs(ymax - y); + if (x_diff < NEAREST) + x_diff = NEAREST; + if (y_diff < NEAREST) + y_diff = NEAREST; + double z; + if (x_diff < y_diff) { + z = y_diff / Math.tan(Math.PI / 18.0); + } else { + z = x_diff / Math.tan(Math.PI / 18.0); + } + Vector3d eye = new Vector3d(center.dot(vx), center.dot(vy), center.dot(vz)); + if (cameraBack != null) { + eye.add(cameraBack); + } else { + eye.z += z; + } + + matrix[0] = vx.x; matrix[1] = vx.y; matrix[2] = vx.z; matrix[3] = -eye.x; + matrix[4] = vy.x; matrix[5] = vy.y; matrix[6] = vy.z; matrix[7] = -eye.y; + matrix[8] = vz.x; matrix[9] = vz.y; matrix[10] = vz.z; matrix[11] = -eye.z; + matrix[12] = 0.0; matrix[13] = 0.0; matrix[14] = 0.0; matrix[15] = 1.0; + worldToView.set(matrix); + try { + worldToView.invert(); + } catch (SingularMatrixException e) { + return; + } + } + try { + if (viewPlatformTransform != null) viewPlatformTransform.setTransform(worldToView); + } catch (BadTransformException e) { + + } + } + + public Position3D getViewPoint() { + if (viewPoint != null) return viewPoint; + if (viewPointObj != null) return viewPointObj.getPosition3D(); + return null; + } + + public Vector3d getViewLine() { + return viewLine; + } + + public void setWorldToView(Position3D vp, Vector3d vl) { + // カメラパラメータへの反映 + if (viewPoint != null) { + viewPoint.setVector3d(vp.getVector3d()); + } else if (viewPointObj != null) { + viewPointObj.apply(vp, false); + } + if (viewLine != null) { + viewLine.set(vl); + } + + // カメラ座標系(vx, vy, vz)を計算する + Vector3d vx = new Vector3d(), vy = new Vector3d(), vz = new Vector3d(); + viewLine.normalize(); + vz.negate(viewLine); + vx.cross(vz, VIEW_DOWN); + if (vx.length() > GeometryUtility.TOLERANCE) { + vx.normalize(); + } else { + vx = new Vector3d(1.0, 0.0, 0.0); + } + vy.cross(vz, vx); + matrix[0] = vx.x; matrix[1] = vx.y; matrix[2] = vx.z; matrix[3] = 0.0; + matrix[4] = vy.x; matrix[5] = vy.y; matrix[6] = vy.z; matrix[7] = 0.0; + matrix[8] = vz.x; matrix[9] = vz.y; matrix[10] = vz.z; matrix[11] = 0.0; + matrix[12] = 0.0; matrix[13] = 0.0; matrix[14] = 0.0; matrix[15] = 1.0; + worldToView.set(matrix); + try { + worldToView.invert(); + } catch (SingularMatrixException e) { + return; + } + worldToView.setTranslation(vp.getVector3d()); + try { + if (viewPlatformTransform != null) viewPlatformTransform.setTransform(worldToView); + } catch (BadTransformException e) { + } + } + + public Transform3D getWorldToView() { + adjust(1L); + Transform3D trans = new Transform3D(); + trans.set(matrix); + trans.invert(); + return trans; + } + + public void transformView(Transform3D delta) { + viewPlatformTransform.getTransform(cameraTransform); + cameraTransform.mul(delta); + try { + viewPlatformTransform.setTransform(cameraTransform); + } catch (BadTransformException e) { + + } + } +} diff --git a/src/main/java/framework/view3D/Dot3BumpMapShader.java b/src/main/java/framework/view3D/Dot3BumpMapShader.java new file mode 100644 index 0000000..8eaccbc --- /dev/null +++ b/src/main/java/framework/view3D/Dot3BumpMapShader.java @@ -0,0 +1,143 @@ +package framework.view3D; + +import framework.model3D.BumpMapGenerator; + +import javax.media.j3d.*; +import javax.vecmath.Vector3f; +import java.awt.image.BufferedImage; + +public class Dot3BumpMapShader { + private TextureCubeMap normalMappingTexture = null; + private static Vector3f yAxis = new Vector3f(0.0f, 1.0f, 0.0f); + + public void init(DirectionalLight dirlight) { + Vector3f lightDir = new Vector3f(); + dirlight.getDirection(lightDir); + lightDir.negate(); // 法線方向が光線と逆向きの面が一番明るい + lightDir.normalize(); + + BufferedImage topLightMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + BufferedImage bottomLightMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + BufferedImage eastLightMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + BufferedImage westLightMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + BufferedImage northLightMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + BufferedImage southLightMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f(-1.0f, ((float)t - 256.0f) / 256.0f, ((float)s - 256.0f) / 256.0f); + int rgb = calcRelativeLightDir(lightDir, texZ); + southLightMapping.setRGB(s, t, rgb); + } + } + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f(1.0f, ((float)t - 256.0f) / 256.0f, (256.0f - (float)s) / 256.0f); + int rgb = calcRelativeLightDir(lightDir, texZ); + northLightMapping.setRGB(s, t, rgb); + } + } + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f(((float)s - 256.0f) / 256.0f, ((float)t - 256.0f) / 256.0f, 1.0f); + int rgb = calcRelativeLightDir(lightDir, texZ); + eastLightMapping.setRGB(s, t, rgb); + } + } + + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f((256.0f - (float)s) / 256.0f, ((float)t - 256.0f) / 256.0f, -1.0f); + int rgb = calcRelativeLightDir(lightDir, texZ); + westLightMapping.setRGB(s, t, rgb); + } + } + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f(((float)s - 256.0f) / 256.0f, 1.0f, (256.0f - (float)t) / 256.0f); + int rgb = calcRelativeLightDir(lightDir, texZ); + topLightMapping.setRGB(s, t, rgb); + } + } + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f(((float)s - 256.0f) / 256.0f, -1.0f, ((float)t - 256.0f) / 256.0f); + int rgb = calcRelativeLightDir(lightDir, texZ); + bottomLightMapping.setRGB(s, t, rgb); + } + } + + ImageComponent2D topImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, topLightMapping, true, false); + ImageComponent2D bottomImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, bottomLightMapping, true, false); + ImageComponent2D eastImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, eastLightMapping, true, false); + ImageComponent2D westImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, westLightMapping, true, false); + ImageComponent2D northImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, northLightMapping, true, false); + ImageComponent2D southImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, southLightMapping, true, false); + normalMappingTexture = new TextureCubeMap(TextureCubeMap.BASE_LEVEL, TextureCubeMap.RGB, topLightMapping.getWidth()); + normalMappingTexture.setImage(0, TextureCubeMap.POSITIVE_Y, topImage); + normalMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_Y, bottomImage); + normalMappingTexture.setImage(0, TextureCubeMap.POSITIVE_X, northImage); + normalMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_X, southImage); + normalMappingTexture.setImage(0, TextureCubeMap.POSITIVE_Z, eastImage); + normalMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_Z, westImage); + } + + public void updateAppearance(Appearance ap, BumpMapGenerator bumpMapGenerator, Camera3D camera) { + if (normalMappingTexture == null) return; + if (!bumpMapGenerator.hasMapped()) { + int n = ap.getTextureUnitCount(); + TextureUnitState newUnitStates[] = new TextureUnitState[n + 2]; + if (ap.getTextureUnitState() != null) System.arraycopy(ap.getTextureUnitState(), 0, newUnitStates, 2, n); + TexCoordGeneration tcg = new TexCoordGeneration(TexCoordGeneration.NORMAL_MAP, TexCoordGeneration.TEXTURE_COORDINATE_3); + TextureAttributes ta = new TextureAttributes(); + ta.setTextureMode(TextureAttributes.REPLACE); + ta.setCapability(TextureAttributes.ALLOW_TRANSFORM_READ); + ta.setCapability(TextureAttributes.ALLOW_TRANSFORM_WRITE); + // TexCoordGeneration.NORMAL_MAP は視線に対して固定なので、視線の向きに合わせてテクスチャを回転 + ta.setTextureTransform(camera.getWorldToView()); + // テクスチャユニットの一番最初に法線マッピングを設定する + newUnitStates[0] = new TextureUnitState(normalMappingTexture, ta, tcg); + + ta = new TextureAttributes(); + ta.setTextureMode(TextureAttributes.COMBINE); + ta.setCombineRgbMode(TextureAttributes.COMBINE_DOT3); + ta.setCapability(TextureAttributes.ALLOW_TRANSFORM_READ); + ta.setCapability(TextureAttributes.ALLOW_TRANSFORM_WRITE); + // テクスチャユニットの最初から二番目にバンプマッピングを設定する + newUnitStates[1] = new TextureUnitState(bumpMapGenerator.getBumpMap(), ta, bumpMapGenerator.getTexCoordGeneration()); + + ap.setTextureUnitState(newUnitStates); + bumpMapGenerator.setMapped(); + } else { + TextureAttributes ta = ap.getTextureUnitState(0).getTextureAttributes(); + // TexCoordGeneration.NORMZAL_MAP は視線に対して固定なので、視線の向きに合わせてテクスチャを回転 + ta.setTextureTransform(camera.getWorldToView()); + } + } + + private int calcRelativeLightDir(Vector3f lightDir, Vector3f texZ) { + texZ.normalize(); + Vector3f texX = new Vector3f(); + texX.cross(yAxis, texZ); + if (texX.length() == 0.0f) { + texX = new Vector3f(1.0f, 0.0f, 0.0f); + } else { + texX.normalize(); + } + Vector3f texY = new Vector3f(); + texY.cross(texZ, texX); + float x = lightDir.dot(texX); + float y = lightDir.dot(texY); + float z = lightDir.dot(texZ); + int r = (int)(x * 127.5f + 127.5f); + int g = (int)(y * 127.5f + 127.5f); + int b = (int)(z * 127.5f + 127.5f); + int rgb = (r << 16) + (g << 8) + b; + return rgb; + } +} diff --git a/src/main/java/framework/view3D/FlatDot3BumpMapShader.java b/src/main/java/framework/view3D/FlatDot3BumpMapShader.java new file mode 100644 index 0000000..7d57303 --- /dev/null +++ b/src/main/java/framework/view3D/FlatDot3BumpMapShader.java @@ -0,0 +1,59 @@ +package framework.view3D; + +import framework.model3D.BumpMapGenerator; + +import javax.media.j3d.*; +import javax.vecmath.Color3f; +import javax.vecmath.Vector3f; + +public class FlatDot3BumpMapShader { + private static Vector3f yAxis = new Vector3f(0.0f, 1.0f, 0.0f); + private static Color3f color; + + public void init(DirectionalLight dirlight) { + Vector3f lightDir = new Vector3f(); + dirlight.getDirection(lightDir); + lightDir.negate(); // 法線方向が光線と逆向きの面が一番明るい + lightDir.normalize(); + color = calcRelativeLightDir(lightDir, yAxis); + } + + public void updateAppearance(Appearance ap, BumpMapGenerator bumpMapGenerator, Camera3D camera) { + if (!bumpMapGenerator.hasMapped()) { + ap.setMaterial(null); + ColoringAttributes ca = new ColoringAttributes(); + ca.setColor(color); + ap.setColoringAttributes(ca); + int n = ap.getTextureUnitCount(); + TextureUnitState newUnitStates[] = new TextureUnitState[n + 1]; + if (ap.getTextureUnitState() != null) System.arraycopy(ap.getTextureUnitState(), 0, newUnitStates, 1, n); + TextureAttributes ta = new TextureAttributes(); + ta.setTextureMode(TextureAttributes.COMBINE); + ta.setCombineRgbMode(TextureAttributes.COMBINE_DOT3); + ta.setCapability(TextureAttributes.ALLOW_TRANSFORM_READ); + ta.setCapability(TextureAttributes.ALLOW_TRANSFORM_WRITE); + // テクスチャユニットの最初から二番目にバンプマッピングを設定する + newUnitStates[0] = new TextureUnitState(bumpMapGenerator.getBumpMap(), ta, bumpMapGenerator.getTexCoordGeneration()); + + ap.setTextureUnitState(newUnitStates); + bumpMapGenerator.setMapped(); + } + } + + private Color3f calcRelativeLightDir(Vector3f lightDir, Vector3f texZ) { + texZ.normalize(); + Vector3f texX = new Vector3f(); + texX.cross(yAxis, texZ); + if (texX.length() == 0.0f) { + texX = new Vector3f(1.0f, 0.0f, 0.0f); + } else { + texX.normalize(); + } + Vector3f texY = new Vector3f(); + texY.cross(texZ, texX); + float x = lightDir.dot(texX); + float y = lightDir.dot(texY); + float z = lightDir.dot(texZ); + return new Color3f((x + 1.0f) / 2.0f, (y + 1.0f) / 2.0f, (z + 1.0f) / 2.0f); + } +} diff --git a/src/main/java/framework/view3D/FresnelsReflectionMapShader.java b/src/main/java/framework/view3D/FresnelsReflectionMapShader.java new file mode 100644 index 0000000..b6714f7 --- /dev/null +++ b/src/main/java/framework/view3D/FresnelsReflectionMapShader.java @@ -0,0 +1,176 @@ +package framework.view3D; + +import framework.model3D.BackgroundBox; +import framework.model3D.FresnelsReflectionMapGenerator; + +import javax.media.j3d.*; +import javax.vecmath.Vector3f; +import java.awt.image.BufferedImage; + +public class FresnelsReflectionMapShader { + boolean bTransparent = false; + TextureCubeMap reflectionMappingTexture = null; + TextureCubeMap fresnelsMappingTexture = null; + + public void init(BackgroundBox skyBox) { + // 環境反射のテクスチャ + reflectionMappingTexture = new TextureCubeMap(TextureCubeMap.BASE_LEVEL, TextureCubeMap.RGBA, skyBox.getTopImage().getWidth()); + ImageComponent2D topSkyImage = skyBox.getTopImage(); + ImageComponent2D bottomSkyImage = skyBox.getBottomImage(); + ImageComponent2D eastSkyImage = skyBox.getEastImage(); + ImageComponent2D westSkyImage = skyBox.getWestImage(); + ImageComponent2D northSkyImage = skyBox.getNorthImage(); + ImageComponent2D southSkyImage = skyBox.getSouthImage(); + reflectionMappingTexture.setImage(0, TextureCubeMap.POSITIVE_Y, topSkyImage); + reflectionMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_Y, bottomSkyImage); + reflectionMappingTexture.setImage(0, TextureCubeMap.POSITIVE_Z, westSkyImage); + reflectionMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_Z, eastSkyImage); + reflectionMappingTexture.setImage(0, TextureCubeMap.POSITIVE_X, northSkyImage); + reflectionMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_X, southSkyImage); + + // フレネル反射係数を計算しテクスチャに埋め込む + BufferedImage topFresnelsMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + BufferedImage bottomFresnelsMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + BufferedImage eastFresnelsMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + BufferedImage westFresnelsMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + BufferedImage northFresnelsMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + BufferedImage southFresnelsMapping = new BufferedImage(512, 512, BufferedImage.TYPE_INT_ARGB); + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f(-1.0f, ((float)t - 256.0f) / 256.0f, ((float)s - 256.0f) / 256.0f); + int rgb = calcFresnels(texZ); + southFresnelsMapping.setRGB(s, t, rgb); + } + } + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f(1.0f, ((float)t - 256.0f) / 256.0f, (256.0f - (float)s) / 256.0f); + int rgb = calcFresnels(texZ); + northFresnelsMapping.setRGB(s, t, rgb); + } + } + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f(((float)s - 256.0f) / 256.0f, ((float)t - 256.0f) / 256.0f, 1.0f); + int rgb = calcFresnels(texZ); + eastFresnelsMapping.setRGB(s, t, rgb); + } + } + + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f((256.0f - (float)s) / 256.0f, ((float)t - 256.0f) / 256.0f, -1.0f); + int rgb = calcFresnels(texZ); + westFresnelsMapping.setRGB(s, t, rgb); + } + } + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f(((float)s - 256.0f) / 256.0f, 1.0f, (256.0f - (float)t) / 256.0f); + int rgb = calcFresnels(texZ); + topFresnelsMapping.setRGB(s, t, rgb); + } + } + + for (int t = 0; t < 512; t++) { + for (int s = 0; s < 512; s++) { + Vector3f texZ = new Vector3f(((float)s - 256.0f) / 256.0f, -1.0f, ((float)t - 256.0f) / 256.0f); + int rgb = calcFresnels(texZ); + bottomFresnelsMapping.setRGB(s, t, rgb); + } + } + + ImageComponent2D topImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, topFresnelsMapping, true, false); + ImageComponent2D bottomImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, bottomFresnelsMapping, true, false); + ImageComponent2D eastImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, eastFresnelsMapping, true, false); + ImageComponent2D westImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, westFresnelsMapping, true, false); + ImageComponent2D northImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, northFresnelsMapping, true, false); + ImageComponent2D southImage = new ImageComponent2D(ImageComponent2D.FORMAT_RGBA, southFresnelsMapping, true, false); + fresnelsMappingTexture = new TextureCubeMap(TextureCubeMap.BASE_LEVEL, TextureCubeMap.RGBA, topFresnelsMapping.getWidth()); + fresnelsMappingTexture.setImage(0, TextureCubeMap.POSITIVE_Y, topImage); + fresnelsMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_Y, bottomImage); + fresnelsMappingTexture.setImage(0, TextureCubeMap.POSITIVE_X, northImage); + fresnelsMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_X, southImage); + fresnelsMappingTexture.setImage(0, TextureCubeMap.POSITIVE_Z, eastImage); + fresnelsMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_Z, westImage); + } + + public void updateAppearance(Appearance ap, FresnelsReflectionMapGenerator reflectionMapGenerator, Camera3D camera) { + if (reflectionMappingTexture == null) return; + if (!reflectionMapGenerator.hasMapped()) { + TexCoordGeneration tcg1 = new TexCoordGeneration(TexCoordGeneration.REFLECTION_MAP, TexCoordGeneration.TEXTURE_COORDINATE_3); + TextureAttributes ta1 = new TextureAttributes(); + ta1.setTextureMode(TextureAttributes.COMBINE); + ta1.setCombineRgbMode(TextureAttributes.COMBINE_ADD_SIGNED); +// ta1.setCombineRgbMode(TextureAttributes.COMBINE_INTERPOLATE); + ta1.setCombineRgbSource(0, TextureAttributes.COMBINE_TEXTURE_COLOR); + ta1.setCombineRgbSource(1, TextureAttributes.COMBINE_PREVIOUS_TEXTURE_UNIT_STATE); + ta1.setCombineRgbSource(2, TextureAttributes.COMBINE_CONSTANT_COLOR); + ta1.setCombineRgbFunction(0, TextureAttributes.COMBINE_SRC_COLOR); + ta1.setCombineRgbFunction(1, TextureAttributes.COMBINE_SRC_COLOR); + ta1.setCombineRgbFunction(2, TextureAttributes.COMBINE_SRC_COLOR); + ta1.setTextureBlendColor(reflectionMapGenerator.getBlendColor()); + ta1.setCapability(TextureAttributes.ALLOW_TRANSFORM_READ); + ta1.setCapability(TextureAttributes.ALLOW_TRANSFORM_WRITE); + // TexCoordGeneration.REFLECTION_MAP は視線に対して固定なので、視線の向きに合わせてテクスチャを回転 + ta1.setTextureTransform(camera.getWorldToView()); + + TexCoordGeneration tcg2 = new TexCoordGeneration(TexCoordGeneration.REFLECTION_MAP, TexCoordGeneration.TEXTURE_COORDINATE_3); + TextureAttributes ta2 = new TextureAttributes(); + + if (reflectionMapGenerator.isTransparent()) { + ta2.setTextureMode(TextureAttributes.BLEND); + } else { + ta2.setTextureMode(TextureAttributes.COMBINE); + ta2.setCombineRgbMode(TextureAttributes.COMBINE_INTERPOLATE); + ta2.setCombineRgbSource(0, TextureAttributes.COMBINE_PREVIOUS_TEXTURE_UNIT_STATE); + ta2.setCombineRgbSource(1, TextureAttributes.COMBINE_OBJECT_COLOR); + ta2.setCombineRgbSource(2, TextureAttributes.COMBINE_TEXTURE_COLOR); + ta2.setCombineRgbFunction(0, TextureAttributes.COMBINE_SRC_COLOR); + ta2.setCombineRgbFunction(1, TextureAttributes.COMBINE_SRC_COLOR); + ta2.setCombineRgbFunction(2, TextureAttributes.COMBINE_SRC_ALPHA); + } + + int n = ap.getTextureUnitCount(); + if (n > 0) { + // テクスチャユニットの一番最後に反射マッピングとフレネル反射係数を設定する + TextureUnitState newUnitStates[] = new TextureUnitState[n + 2]; + if (ap.getTextureUnitState() != null) System.arraycopy(ap.getTextureUnitState(), 0, newUnitStates, 0, n); + newUnitStates[n] = new TextureUnitState(reflectionMappingTexture, ta1, tcg1); + newUnitStates[n].setCapability(TextureUnitState.ALLOW_STATE_READ); + newUnitStates[n].setCapability(TextureUnitState.ALLOW_STATE_WRITE); + newUnitStates[n+1] = new TextureUnitState(fresnelsMappingTexture, ta2, tcg2); + + ap.setTextureUnitState(newUnitStates); + } else { + TextureUnitState newUnitStates[] = new TextureUnitState[2]; + newUnitStates[0] = new TextureUnitState(reflectionMappingTexture, ta1, tcg1); + newUnitStates[0].setCapability(TextureUnitState.ALLOW_STATE_READ); + newUnitStates[0].setCapability(TextureUnitState.ALLOW_STATE_WRITE); + newUnitStates[1] = new TextureUnitState(fresnelsMappingTexture, ta2, tcg2); + + ap.setTextureUnitState(newUnitStates); + } + reflectionMapGenerator.setMapped(); + } else { + int n = ap.getTextureUnitCount(); + if (n > 1) { + TextureAttributes ta = ap.getTextureUnitState(n - 2).getTextureAttributes(); + // TexCoordGeneration.REFLECTION_MAP は視線に対して固定なので、視線の向きに合わせてテクスチャを回転 + ta.setTextureTransform(camera.getWorldToView()); + } + } + } + + private int calcFresnels(Vector3f texZ) { + texZ.normalize(); + int a = (int)(-texZ.z * 127.5f + 127.5f); + int rgb = (a << 24) + (0 << 16) + (0 << 8) + 0; + return rgb; + } +} diff --git a/src/main/java/framework/view3D/ReflectionMapShader.java b/src/main/java/framework/view3D/ReflectionMapShader.java new file mode 100644 index 0000000..828c61e --- /dev/null +++ b/src/main/java/framework/view3D/ReflectionMapShader.java @@ -0,0 +1,72 @@ +package framework.view3D; + +import framework.model3D.BackgroundBox; +import framework.model3D.ReflectionMapGenerator; + +import javax.media.j3d.*; + +public class ReflectionMapShader { + TextureCubeMap reflectionMappingTexture = null; + + public void init(BackgroundBox skyBox) { + reflectionMappingTexture = new TextureCubeMap(TextureCubeMap.BASE_LEVEL, TextureCubeMap.RGB, skyBox.getTopImage().getWidth()); + ImageComponent2D topImage = (ImageComponent2D)skyBox.getTopImage(); + ImageComponent2D bottomImage = (ImageComponent2D)skyBox.getBottomImage(); + ImageComponent2D eastImage = (ImageComponent2D)skyBox.getEastImage(); + ImageComponent2D westImage = (ImageComponent2D)skyBox.getWestImage(); + ImageComponent2D northImage = (ImageComponent2D)skyBox.getNorthImage(); + ImageComponent2D southImage = (ImageComponent2D)skyBox.getSouthImage(); + reflectionMappingTexture.setImage(0, TextureCubeMap.POSITIVE_Y, topImage); + reflectionMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_Y, bottomImage); + reflectionMappingTexture.setImage(0, TextureCubeMap.POSITIVE_Z, eastImage); + reflectionMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_Z, westImage); + reflectionMappingTexture.setImage(0, TextureCubeMap.POSITIVE_X, northImage); + reflectionMappingTexture.setImage(0, TextureCubeMap.NEGATIVE_X, southImage); + } + + + public void updateAppearance(Appearance ap, ReflectionMapGenerator reflectionMapGenerator, Camera3D camera) { + if (reflectionMappingTexture == null) return; + if (!reflectionMapGenerator.hasMapped()) { + TexCoordGeneration tcg = new TexCoordGeneration(TexCoordGeneration.REFLECTION_MAP, TexCoordGeneration.TEXTURE_COORDINATE_3); + TextureAttributes ta = new TextureAttributes(); + ta.setTextureMode(TextureAttributes.COMBINE); + ta.setCombineRgbMode(TextureAttributes.COMBINE_INTERPOLATE); + ta.setCombineRgbSource(0, TextureAttributes.COMBINE_TEXTURE_COLOR); + ta.setCombineRgbSource(1, TextureAttributes.COMBINE_PREVIOUS_TEXTURE_UNIT_STATE); + ta.setCombineRgbSource(2, TextureAttributes.COMBINE_CONSTANT_COLOR); + ta.setCombineRgbFunction(0, TextureAttributes.COMBINE_SRC_COLOR); + ta.setCombineRgbFunction(1, TextureAttributes.COMBINE_SRC_COLOR); + ta.setCombineRgbFunction(2, TextureAttributes.COMBINE_SRC_COLOR); + ta.setTextureBlendColor(reflectionMapGenerator.getBlendColor()); + ta.setCapability(TextureAttributes.ALLOW_TRANSFORM_READ); + ta.setCapability(TextureAttributes.ALLOW_TRANSFORM_WRITE); + // TexCoordGeneration.REFLECTION_MAP は視線に対して固定なので、視線の向きに合わせてテクスチャを回転 + ta.setTextureTransform(camera.getWorldToView()); + int n = ap.getTextureUnitCount(); + if (n > 0) { + // テクスチャユニットの一番最後に反射マッピングを設定する + TextureUnitState newUnitStates[] = new TextureUnitState[n + 1]; + if (ap.getTextureUnitState() != null && n > 0) System.arraycopy(ap.getTextureUnitState(), 0, newUnitStates, 0, n); + newUnitStates[n] = new TextureUnitState(reflectionMappingTexture, ta, tcg); + + ap.setTextureUnitState(newUnitStates); + } else { + ap.setTexture(reflectionMappingTexture); + ap.setTextureAttributes(ta); + ap.setTexCoordGeneration(tcg); + } + reflectionMapGenerator.setMapped(); + } else { + TextureAttributes ta; + int n = ap.getTextureUnitCount(); + if (n > 0) { + ta = ap.getTextureUnitState(ap.getTextureUnitCount() - 1).getTextureAttributes(); + } else { + ta = ap.getTextureAttributes(); + } + // TexCoordGeneration.REFLECTION_MAP は視線に対して固定なので、視線の向きに合わせてテクスチャを回転 + ta.setTextureTransform(camera.getWorldToView()); + } + } +} diff --git a/src/main/java/framework/view3D/Viewer3D.java b/src/main/java/framework/view3D/Viewer3D.java new file mode 100644 index 0000000..1e3a593 --- /dev/null +++ b/src/main/java/framework/view3D/Viewer3D.java @@ -0,0 +1,192 @@ +package framework.view3D; + +import com.sun.j3d.utils.geometry.*; +import framework.model3D.*; + +import javax.media.j3d.*; +import javax.vecmath.Point3f; +import java.util.ArrayList; +import java.util.Enumeration; + +public class Viewer3D implements IViewer3D { + GraphicsContext3D graphicsContext3D = null; + ReflectionMapShader reflectionMappingShader = null; + FresnelsReflectionMapShader fresnelsReflectionMappingShader = null; + Dot3BumpMapShader dot3BumpMappingShader = null; + FlatDot3BumpMapShader flatDot3BumpMappingShader = null; + ArrayList lights = null; + BackgroundBox skyBox = null; + Camera3D camera = null; + + // 以下は省メモリ化のため導入 + static private Point3f center = new Point3f(); + static private Point3f origin = new Point3f(); + + public Viewer3D(Camera3D camera) { + this.camera = camera; + reflectionMappingShader = new ReflectionMapShader(); + fresnelsReflectionMappingShader = new FresnelsReflectionMapShader(); + dot3BumpMappingShader = new Dot3BumpMapShader(); + flatDot3BumpMappingShader = new FlatDot3BumpMapShader(); + } + + @Override + public void setGraphicsContext3D(GraphicsContext3D graphicsContext3D) { + this.graphicsContext3D = graphicsContext3D; + } + + + @Override + public void update(ArrayList lights, BackgroundBox skyBox) { + // 光源の更新 + if (this.lights != lights) { + this.lights = lights; + for (int n = 0; n < lights.size(); n++) { + if (lights.get(n) instanceof DirectionalLight) { + // 平行光源の場合 + DirectionalLight dirlight = (DirectionalLight)lights.get(n); + dot3BumpMappingShader.init(dirlight); + flatDot3BumpMappingShader.init(dirlight); + break; + } + } + } + + // スカイボックスの更新 + if (this.skyBox != skyBox) { + this.skyBox = skyBox; + reflectionMappingShader.init(skyBox); + fresnelsReflectionMappingShader.init(skyBox); + } + } + + public void draw(BaseObject3D obj) { + + // GraphicsContext3D 内の光源の更新 + if (lights != null && lights.size() != graphicsContext3D.numLights()) { + for (int n = 0; n < lights.size(); n++) { + graphicsContext3D.addLight(lights.get(n)); + } + } + + Enumeration primitives = obj.getPrimitiveNodes(); + ReflectionMapGenerator reflectionMapGenerator = obj.getReflectionMappingInfo(); + BumpMapGenerator bumpMapGenerator = obj.getBumpMappingInfo(); + int n = 0; + while (primitives.hasMoreElements()) { + if (obj instanceof Object3D && ((Object3D)obj).isLODSet()) { + // LOD の判定 + LOD lodNode = ((Object3D)obj).getLOD(); + if (lodNode instanceof DistanceLOD) { + double nearSide = 0.0; + double farSide = 0.0; + DistanceLOD distanceLOD = ((DistanceLOD)lodNode); + if (n > distanceLOD.numDistances()) break; + if (n > 0) nearSide = distanceLOD.getDistance(n - 1); + if (n < distanceLOD.numDistances()) farSide = distanceLOD.getDistance(n); + + distanceLOD.getPosition(center); + Transform3D trans = new Transform3D(); + graphicsContext3D.getModelTransform(trans); + trans.transform(center); // オブジェクトの中心座標(世界座標上) + trans = camera.getWorldToView(); + trans.transform(center); // オブジェクトの中心座標(カメラ座標上) + double distance = (double)center.distance(origin); + if (distance < nearSide) { + break; + } else if (distance > farSide && n < distanceLOD.numDistances()) { + n++; + continue; + } + } + } + Node primitive = primitives.nextElement(); + if (primitive instanceof Shape3D) { + // Shape3Dをレンダリング + Appearance ap = ((Shape3D)primitive).getAppearance(); + if (bumpMapGenerator != null) { + // obj にはバンプマッピングが設定されている + if (!bumpMapGenerator.isHorizontal()) { + dot3BumpMappingShader.updateAppearance(ap, bumpMapGenerator, camera); + } else { + // 水平のポリゴンに対しては、よりテクスチャユニットの消費量が少ない FlatDot3BumpMapShader を使う + flatDot3BumpMappingShader.updateAppearance(ap, bumpMapGenerator, camera); + } + } + if (reflectionMapGenerator != null) { + // obj には反射マッピングが設定されている + if (reflectionMapGenerator instanceof FresnelsReflectionMapGenerator) { + // フレネル反射を使用 + fresnelsReflectionMappingShader.updateAppearance(ap, (FresnelsReflectionMapGenerator)reflectionMapGenerator, camera); + } else { + reflectionMappingShader.updateAppearance(ap, reflectionMapGenerator, camera); + } + } + + if (((Shape3D)primitive).getGeometry() != null) { + graphicsContext3D.draw((Shape3D)primitive); + } + } else if (primitive instanceof Primitive) { + Appearance ap = ((Primitive)primitive).getAppearance(); + if (bumpMapGenerator != null) { + // obj にはバンプマッピングが設定されている + if (!bumpMapGenerator.isHorizontal()) { + dot3BumpMappingShader.updateAppearance(ap, bumpMapGenerator, camera); + } else { + // 水平のポリゴンに対しては、よりテクスチャユニットの消費量が少ない FlatDot3BumpMapShader を使う + flatDot3BumpMappingShader.updateAppearance(ap, bumpMapGenerator, camera); + } + } + if (reflectionMapGenerator != null) { + // obj には反射マッピングが設定されている + if (reflectionMapGenerator instanceof FresnelsReflectionMapGenerator) { + // フレネル反射を使用 + fresnelsReflectionMappingShader.updateAppearance(ap, (FresnelsReflectionMapGenerator)reflectionMapGenerator, camera); + } else { + reflectionMappingShader.updateAppearance(ap, reflectionMapGenerator, camera); + } + } + graphicsContext3D.setAppearance(ap); + if (primitive instanceof Cone) { + // Coneをレンダリング + Geometry g = ((Cone)primitive).getShape(Cone.BODY).getGeometry(); + graphicsContext3D.draw(g); + g = ((Cone)primitive).getShape(Cone.CAP).getGeometry(); + graphicsContext3D.draw(g); + } else if (primitive instanceof Cylinder) { + // Cylinderをレンダリング + Geometry g = ((Cylinder)primitive).getShape(Cylinder.BODY).getGeometry(); + graphicsContext3D.draw(g); + g = ((Cylinder)primitive).getShape(Cylinder.TOP).getGeometry(); + graphicsContext3D.draw(g); + g = ((Cylinder)primitive).getShape(Cylinder.BOTTOM).getGeometry(); + graphicsContext3D.draw(g); + } else if (primitive instanceof Box) { + // Boxをレンダリング + Geometry g = ((Box)primitive).getShape(Box.TOP).getGeometry(); + graphicsContext3D.draw(g); + g = ((Box)primitive).getShape(Box.BOTTOM).getGeometry(); + graphicsContext3D.draw(g); + g = ((Box)primitive).getShape(Box.FRONT).getGeometry(); + graphicsContext3D.draw(g); + g = ((Box)primitive).getShape(Box.BACK).getGeometry(); + graphicsContext3D.draw(g); + g = ((Box)primitive).getShape(Box.LEFT).getGeometry(); + graphicsContext3D.draw(g); + g = ((Box)primitive).getShape(Box.RIGHT).getGeometry(); + graphicsContext3D.draw(g); + } else if (primitive instanceof Sphere) { + // Sphereをレンダリング + Geometry g = ((Sphere)primitive).getShape().getGeometry(); + graphicsContext3D.draw(g); + } + } + n++; + } + } + + @Override + public void setModelTransform(Transform3D t) { + graphicsContext3D.setModelTransform(t); + } +} diff --git a/src/main/java/org/ntlab/SproutServer/accounts/Account.java b/src/main/java/org/ntlab/SproutServer/accounts/Account.java new file mode 100644 index 0000000..4d50c76 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/accounts/Account.java @@ -0,0 +1,56 @@ +package org.ntlab.SproutServer.accounts; + +import java.net.URI; + +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriBuilder; + +public class Account { + + private String userName; //ユーザーネーム + private String mode = null; //モード選択 + private int userID; //ユーザID + + public Account(int userID,String userName){ + this.userID = userID; + this.userName = userName; + } + + public Account(){ + + } + + public static Account getInstance() { + return getInstance(); + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public int getUserID() { + return userID; + } + + public void setUserID(int userID) { + this.userID = userID; + } +} diff --git a/src/main/java/org/ntlab/SproutServer/accounts/Accounts.java b/src/main/java/org/ntlab/SproutServer/accounts/Accounts.java new file mode 100644 index 0000000..8e3cd50 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/accounts/Accounts.java @@ -0,0 +1,74 @@ +package org.ntlab.SproutServer.accounts; + +import javax.inject.Singleton; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import java.util.ArrayList; + + +public class Accounts { + + private static Accounts theInstance = null; + private ArrayList accounts = new ArrayList(); + private int userID = 0; + + public Accounts() { + if (theInstance == null) { + theInstance = this; + } + } + + public static Accounts getInstance() { + if (theInstance == null) { + theInstance = new Accounts(); + } + return theInstance; + } + + + public void createAccount(int userId){ + Account newAccount = new Account(userId, "user"+userId); + accounts.add(newAccount); + System.out.println(accounts.size()); + } + + + public Accounts getAccount() { + Account ac = new Account(); + this.userID = ac.getUserID(); + return new Accounts(); + } + + public boolean checkCreatedUser(int userId) { + for (Account account : accounts) { + if (account.getUserID() == userId) { + return true; + } + } + return false; + } + + public ArrayList getAccounts() { + return accounts; + } + + public void setAccounts(ArrayList accounts) { + this.accounts = accounts; + } + + public String getUserNameById(int userId){ + for (Account account: accounts){ + if (account.getUserID() == userId) + return account.getUserName(); + } + return "nameNone"; + } + + public int getUserID() { + return userID; + } + + public void setUserID(int userId){ + this.userID = userId; + } +} \ No newline at end of file diff --git a/src/main/java/org/ntlab/SproutServer/accounts/AccountsRest.java b/src/main/java/org/ntlab/SproutServer/accounts/AccountsRest.java new file mode 100644 index 0000000..7686bce --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/accounts/AccountsRest.java @@ -0,0 +1,55 @@ +package org.ntlab.SproutServer.accounts; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import java.util.ArrayList; + +@Path("accounts") +public class AccountsRest { + Accounts accounts; + + @POST + @Produces(MediaType.APPLICATION_JSON) + public Account createAcount(@FormParam("userName") String userName) { + int userId = accounts.getUserID(); + System.out.println(userName); + + Account newAccount = new Account(accounts.getUserID(),userName); +// newAccount.setUserName(userName);s +// accounts.add(new Account(this.userID,userName)); + accounts.getAccounts().add(newAccount); + userId++; + accounts.setUserID(userId); + + return newAccount; + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public ArrayList getAccountList() { + return accounts.getAccounts(); + } + + @Path("/{userID}") + @PUT + @Produces(MediaType.APPLICATION_JSON) + public Account updateAcount(@PathParam("userID") String userID, @FormParam("userName") String userName, @FormParam("mode") String mode) { + Account editAccount = accounts.getAccounts().get(Integer.valueOf(userID)); + + editAccount.setUserName(userName); + editAccount.setMode(mode); + + accounts.getAccounts().set(Integer.valueOf(userID), editAccount); + + return editAccount; + } + + @Path("/{userID}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Account getAcount(@PathParam("userID") String userID) { + Account editAccount = accounts.getAccounts().get(Integer.valueOf(userID)); + return editAccount; + } + +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/Battle.java b/src/main/java/org/ntlab/SproutServer/battles/Battle.java new file mode 100644 index 0000000..8a618d9 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/Battle.java @@ -0,0 +1,255 @@ +package org.ntlab.SproutServer.battles; + +import framework.model3D.Placeable; +import framework.model3D.Position3D; +import framework.model3D.Universe; +import framework.physics.Ground; +import org.ntlab.SproutServer.rooms.Room; +import org.ntlab.SproutServer.rooms.Rooms; + +import javax.vecmath.Vector3d; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * バトル + * + * @author matsumoto_k + */ +public class Battle { + private HashMap teamMap = new HashMap<>(); // チームの情報を保持 + private double time = BattlesConst.BATTLE_INIT_TIME; // タイム + private Universe universe = null; + private Ground ground = null; + + private int teamId1 = -1; // チームを判別するID + private int teamId2 = -1; // チームを判別するID + /** + * バトルの状態に関する変数 + * battle:対戦中, end:終了 + */ + private String state = "battle"; + + public Battle(Room room1, Room room2) { + universe = new Universe(); + teamId1 = room1.getRoomId(); + teamId2 = room2.getRoomId(); + teamMap.put(room1.getRoomId(), new Team(room1)); + teamMap.put(room2.getRoomId(), new Team(room2)); + + ground = createStage(); + placeStage(ground); + } + + public Team getTeam(int roomId) { + return teamMap.get(roomId); + } + + public HashMap getTeamMap() { + return teamMap; + } + + public void setTeamMap(HashMap teamMap) { + this.teamMap = teamMap; + } + + public double getTime() { + return time; + } + + public void setTime(double time) { + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + /** + * ステージを作成 + * + * @return ステージオブジェクト + */ + private Ground createStage() { + String path = ""; + try { + path = URLDecoder.decode(Battles.getInstance().getClass().getResource("standardStage.obj").getPath(), "utf-8"); + } catch (Exception e) { + System.out.println("error path"); + } + return new StandardStage(path); + } + + /** + * ステージを配置 + * + * @param ground 配置するステージオブジェクト + */ + private void placeStage(Ground ground) { + universe.place(ground); + } + + /** + * idによってチームを取得する + * + * @param teamId 取得したいチームのID + * @return チーム + */ + private Team findTeamByID(int teamId) { + return teamMap.get(teamId); + } + + /** + * 対戦相手のチームを取得 + * + * @param teamId 自身のteamId + * @return 対戦相手のチーム + */ + private Team getOppositeTeam(int teamId) { + if (teamId != teamId1) + return findTeamByID(teamId1); + return findTeamByID(teamId2); + } + + /** + * クライアントからのアップデート + * + * @param userId + * @param teamId + * @param playerData 更新するプレイヤーデータ + */ + public void update(int userId, int teamId, PlayerData playerData) { + Player player = findTeamByID(teamId).findPlayerById(userId); + + Position3D position3D = player.getPlayerPosition3d(); // 更新する前のプレイヤーの位置を記録 + Vector3d vector3d = player.getPlayerVector3d(); // 更新する前のプレイヤーの向きを状態を記録 + + /* プレイヤーのアップデート */ + player.update(playerData.getUserPosition(), playerData.getUserVector()); + + /* プレイヤー同士の衝突判定 */ + boolean collision = checkPlayerCollision(player, userId); + player.setPlayerCollision(collision); + + /* プレイヤー同士が衝突していた時は前の状態に戻す */ + if (player.isPlayerCollision()) { + player.update(position3D, vector3d); + } + + /* playerと相手チームの弾の衝突判定 */ + checkCollisionWithWeapons(player, teamId); + + /* playerDataに弾の情報が含まれている ∧ プレイヤーが生きている 場合実行 */ + if (playerData.isWeaponShoot() && player.isAlive()) { + // switch (Role.getRole(Rooms.getInstance().roomList.get(teamId).getMemberList().get(userId).getRole())){ + //case Gunman: + Bullet bullet = findTeamByID(teamId).createBullet(playerData.getWeaponPosition(), playerData.getWeaponVector(), playerData.getWeaponVelocity()); + universe.place(bullet.getActor()); + // break; + //case Witch: + //Magic magic = findTeamByID(teamId).createMagic(playerData.getWeaponPosition(), playerData.getWeaponVector(), playerData.getWeaponVelocity()); + //universe.place(magic.getActor()); + // break; + //} + } + + /* 生存者による勝利のチェック */ + updateResultByAllDead(); + } + + /** + * プレイヤーの衝突判定 + * + * @param player プレイヤーインスタンス + * @param userId userId + * @return 衝突している時はtrue, していない時はfalse + */ + public boolean checkPlayerCollision(Player player, int userId) { + return findTeamByID(teamId1).checkCollision(player, userId) || findTeamByID(teamId2).checkCollision(player, userId); + } + + private void playerUpdate(Player player, Position3D userPosition, Vector3d userVector) { + player.update(userPosition, userVector); + } + + /** + * プレイヤーと相手チームの武器の衝突判定 + * + * @param player + * @param teamId + */ + public void checkCollisionWithWeapons(Player player, int teamId) { + ArrayList colliedObjects = getOppositeTeam(teamId).getColliedObjects(player); // 取り除くオブジェクトを取得に変更する + for (Placeable obj : colliedObjects) { + universe.displace(obj); + } + } + + /** + * バトルをアップデートする + */ + public void update() { + if (getTime() > 0) { + this.time -= BattlesConst.UPDATE_INTERVAL; + } else { + updateResultByTimeOut(); + return; + } + /* universeに配置している全ての武器を動かす */ + universe.update(BattlesConst.UPDATE_INTERVAL); + checkAllWeaponCollision(findTeamByID(teamId1), findTeamByID(teamId2)); + updateResultByAllDead(); + } + + /** + * 両チームのプレイヤーと弾の当たり判定 + */ + public void checkAllWeaponCollision(Team team1, Team team2) { + ArrayList displaceObj = team1.getColliedObjects(team2); + for (Placeable obj : displaceObj) { + universe.displace(obj); + } + displaceObj = team2.getColliedObjects(team1); + for (Placeable obj : displaceObj) { + universe.displace(obj); + } + } + + /** + * 生存者の数によって勝敗をチェックする + */ + private void updateResultByTimeOut() { + if (findTeamByID(teamId1).getAliveCount() > findTeamByID(teamId2).getAliveCount()) + findTeamByID(teamId1).setResult(true); + else + findTeamByID(teamId2).setResult(true); + finish(); + } + + /** + * 生存者による勝敗のチェック + */ + private void updateResultByAllDead() { + Team team1 = findTeamByID(teamId1); + Team team2 = findTeamByID(teamId2); + + if (team1.isAllPlayerDead() || team2.isAllPlayerDead()) { + team1.setResult(!team1.isAllPlayerDead()); + team2.setResult(!team2.isAllPlayerDead()); + finish(); + } + } + + /** + * バトルを終了させる + */ + public void finish() { + state = "end"; + Rooms.getInstance().getRoomList().get(teamId1).roomInitialize(); + Rooms.getInstance().getRoomList().get(teamId2).roomInitialize(); + } +} \ No newline at end of file diff --git a/src/main/java/org/ntlab/SproutServer/battles/Battles.java b/src/main/java/org/ntlab/SproutServer/battles/Battles.java new file mode 100644 index 0000000..b8fa527 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/Battles.java @@ -0,0 +1,103 @@ +package org.ntlab.SproutServer.battles; + +import net.arnx.jsonic.JSON; +import org.ntlab.SproutServer.accounts.Accounts; +import org.ntlab.SproutServer.rooms.Room; +import org.ntlab.SproutServer.rooms.Rooms; + +import javax.inject.Singleton; +import javax.print.attribute.standard.Media; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; +import java.util.HashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * バトルを管理するクラス + * + * @author matsumoto_k + */ +public class Battles implements Runnable { + private static Battles theInstance = null; // シングルトンパターン + private HashMap battleMap = new HashMap<>(); // バトル一覧 + private int battleCount = 0; // バトルを識別するためのid + private ScheduledThreadPoolExecutor schedule = null; + + @Context + UriInfo uriInfo; + + private Battles() { + System.out.println("create battles singleton"); + schedule = new ScheduledThreadPoolExecutor(1); + schedule.scheduleAtFixedRate(this, BattlesConst.UPDATE_DELAY, BattlesConst.UPDATE_INTERVAL, TimeUnit.MILLISECONDS); + } + + public static Battles getInstance() { + if (theInstance == null) { + theInstance = new Battles(); + } + return theInstance; + } + + // バトル作成 + public int createBattle(Room room1, Room room2) { + Battle battle = new Battle(room1, room2); + battleMap.put(battleCount, battle); + int resBattleCount = battleCount; + battleCount++; + return resBattleCount; + } + + // ダミーバトル作成 + public void createDummyBattle(int battleId, Room room1, Room room2) { + Battle battle = new Battle(room1, room2); + if (battleMap.get(battleId) != null) { + battleMap.remove(battleId); + } + battleMap.put(battleId, battle); + } + + // バトル削除 + public void removeBattle(int battleId) { + battleMap.remove(battleId); + } + + public HashMap getBattleMap() { + return battleMap; + } + + /** + * プレイヤー情報の更新 + * + * @param battleId バトルID + * @param roomId ルームID + * @param userId ユーザーID + * @param playerData 更新するプレイヤーの情報 + */ + public void updatePlayer(int battleId, int roomId, int userId, PlayerData playerData) { + findBattleById(battleId).update(userId, roomId, playerData); + } + + /** + * Battlesのメインループ + */ + @Override + public void run() { + for (Battle battle : battleMap.values()) { + battle.update(); + } + } + + /** + * battleIdによってBattleを取得 + * + * @param battleId 取得したいbattleのbattleId + * @return Battle + */ + public Battle findBattleById(int battleId) { + return battleMap.get(battleId); + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/BattlesConst.java b/src/main/java/org/ntlab/SproutServer/battles/BattlesConst.java new file mode 100644 index 0000000..98e277e --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/BattlesConst.java @@ -0,0 +1,33 @@ +package org.ntlab.SproutServer.battles; + +/** + * バトルで利用する定数 + * + * @author matsumoto_k + */ +public class BattlesConst { + /** + * ゲームをループさせるスレッドのコアプールサイズ + */ + public static final int CORE_POOL_SIZE = 1; + /** + * ゲームをループさせるインターバル(ミリ秒) + */ + public static final int UPDATE_INTERVAL = 100; + /** + * ゲームをループさせる時の初期遅延時間(ミリ秒) + */ + public static final int UPDATE_DELAY = 1000; + /** + * バトルの初期時間(ミリ秒) + */ + public static final long BATTLE_INIT_TIME = 3000000; + /** + * プレイヤーの初期ヒットポイント + */ + public static final int PLAYER_INIT_HP = 1000; + /** + * 弾の寿命(ミリ秒) + */ + public static final long BULLET_LIFE_TIME = 3000; +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/BattlesRest.java b/src/main/java/org/ntlab/SproutServer/battles/BattlesRest.java new file mode 100644 index 0000000..a133434 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/BattlesRest.java @@ -0,0 +1,91 @@ +package org.ntlab.SproutServer.battles; + + +import net.arnx.jsonic.JSON; +import org.ntlab.SproutServer.accounts.Accounts; +import org.ntlab.SproutServer.rooms.Rooms; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import java.util.HashMap; + +/** + * http://nitta-lab-www2.is.konan-u.ac.jp/battles + * 上記URIへの各種リクエストを処理するクラス + * + * @author matsumoto_k + */ +@Path("battles") +public class BattlesRest { + public BattlesRest(){ + + } + + @Path("/test") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hoge() { + return "deploy jenkins from tomcat"; + } + + /** + * バトル一覧取得API + * @return バトル一覧 + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public HashMap getBattle(){ + return Battles.getInstance().getBattleMap(); + } + + /** + * 指定したbattleIdのバトルを取得する + * @param battleId + * @return + */ + @Path("/{battleId}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Battle getBattle(@PathParam("battleId") int battleId) { + return Battles.getInstance().getBattleMap().get(battleId); + } + + /** + * 指定したパラメータのバトルを更新する + * @param battleId + * @param roomId + * @param userId + * @param stringPlayerData + * @return + */ + @Path("/{battleId}") + @PUT + public Battle putBattle(@PathParam("battleId") int battleId, @FormParam("roomId") int roomId, + @FormParam("userId") int userId, @FormParam("playerData") String stringPlayerData) { + PlayerData playerData = JSON.decode(stringPlayerData, PlayerData.class); + Battles.getInstance().updatePlayer(battleId, roomId, userId, playerData); + return Battles.getInstance().findBattleById(battleId); + } + + /** + * ダミーデータの作成 + */ + @Path("/develop") + @GET + public void createDummyBattle() { + Accounts accounts = Accounts.getInstance(); + if (!accounts.checkCreatedUser(1000)) { + for (int i = 1000; i < 1008; i++) { + accounts.createAccount(i); + } + } + Rooms rooms = Rooms.getInstance(); + rooms.makeRoomBattle(1000, 1000, "room1", "hoge"); + + rooms.makeRoomBattle(1001, 1004, "room2", "hoge"); + + int battleId = 1000; + + Battles.getInstance().createDummyBattle(battleId, rooms.getRoomList().get(1000), rooms.getRoomList().get(1001)); + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/Bullet.java b/src/main/java/org/ntlab/SproutServer/battles/Bullet.java new file mode 100644 index 0000000..2b7ce41 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/Bullet.java @@ -0,0 +1,64 @@ +package org.ntlab.SproutServer.battles; + +import framework.model3D.CollisionResult; +import framework.model3D.Object3D; +import framework.model3D.Position3D; +import framework.physics.Solid3D; +import framework.physics.Velocity3D; +import net.arnx.jsonic.JSONHint; + +import javax.vecmath.Vector3d; +import java.net.URLDecoder; + +/** + * プレイヤーの弾 + * + * @author matsumoto_k + */ +public class Bullet extends Weapon { + + private static final int attack = 100; + + public Bullet(Position3D position3D, Vector3d vector3d, Velocity3D velocity3D) { + this.actor = new WeaponActor(new Solid3D(createObject()), null) { + @Override + public void onIntersect(CollisionResult normal, long interval) { + if (normal != null) + alive = false; + } + }; + initWeapon(position3D, vector3d, velocity3D); + } + + @Override + Object3D createObject() { + //TODO:弾のオブジェクトを決める + String path = Battles.getInstance().getClass().getResource("pocha.stl").getPath(); + try { + path = URLDecoder.decode(path, "utf-8"); + } catch (Exception e) { + + } + BulletModel bulletModel = new BulletModel(path, null); + return bulletModel.createObject(); + } + + public Position3D getPosition3d() { + return getActor().getPosition(); + } + + public Vector3d getVector3d() { + return getActor().getDirection(); + } + + @Override + long getInitWeaponLife() { + return BattlesConst.BULLET_LIFE_TIME; + } + + @Override + @JSONHint(ignore = true) + public int getAttack() { + return attack; + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/BulletModel.java b/src/main/java/org/ntlab/SproutServer/battles/BulletModel.java new file mode 100644 index 0000000..64bd5aa --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/BulletModel.java @@ -0,0 +1,24 @@ +package org.ntlab.SproutServer.battles; + +import framework.model3D.ModelFactory; +import framework.model3D.Object3D; + +public class BulletModel { + String modelFileName = null; + String animationFileName = null; + + public BulletModel(String modelFileName, String animationFileName) { + this.modelFileName = modelFileName; + this.animationFileName = animationFileName; + } + + public Object3D createObject() { + Object3D object3D = null; + try { + object3D = ModelFactory.loadModel(modelFileName, false, false).createObject(); + } catch (Exception e) { + + } + return object3D; + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/Magic.java b/src/main/java/org/ntlab/SproutServer/battles/Magic.java new file mode 100644 index 0000000..16df108 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/Magic.java @@ -0,0 +1,62 @@ +package org.ntlab.SproutServer.battles; + +import framework.model3D.CollisionResult; +import framework.model3D.Object3D; +import framework.model3D.Position3D; +import framework.physics.Solid3D; +import framework.physics.Velocity3D; +import net.arnx.jsonic.JSONHint; + +import javax.vecmath.Vector3d; +import java.net.URLDecoder; + +/** + * プレイヤーの魔法 + * + * @author matsumoto_k + */ +public class Magic extends Weapon { + + private static final int attack = 100; + + public Magic(Position3D position3D, Vector3d vector3d, Velocity3D velocity3D) { + this.actor = new WeaponActor(new Solid3D(createObject()), null) { + @Override + public void onIntersect(CollisionResult normal, long interval) { + } + }; + initWeapon(position3D, vector3d, velocity3D); + } + + @Override + Object3D createObject() { + //TODO:弾のオブジェクトを決める + String path = Battles.getInstance().getClass().getResource("pocha.stl").getPath(); + try { + path = URLDecoder.decode(path, "utf-8"); + } catch (Exception e) { + + } + BulletModel bulletModel = new BulletModel(path, null); + return bulletModel.createObject(); + } + + public Position3D getPosition3d() { + return getActor().getPosition(); + } + + public Vector3d getVector3d() { + return getActor().getDirection(); + } + + @Override + long getInitWeaponLife() { + return BattlesConst.BULLET_LIFE_TIME; + } + + @Override + @JSONHint(ignore = true) + public int getAttack() { + return attack; + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/Player.java b/src/main/java/org/ntlab/SproutServer/battles/Player.java new file mode 100644 index 0000000..3fe8e72 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/Player.java @@ -0,0 +1,198 @@ +package org.ntlab.SproutServer.battles; + +import framework.animation.Animation3D; +import framework.gameMain.OvergroundActor; +import framework.model3D.Object3D; +import framework.model3D.Position3D; +import framework.physics.PhysicsUtility; +import framework.physics.Solid3D; +import net.arnx.jsonic.JSONHint; +import org.ntlab.SproutServer.accounts.Accounts; + +import javax.vecmath.Vector3d; +import java.net.URLDecoder; + +/** + * プレイヤー + * + * @author matsumoto_k + */ +public class Player { + + private OvergroundActor actor = null; // プレイヤーのオブジェクト + private int hp = BattlesConst.PLAYER_INIT_HP; // プレイヤーのHP + private boolean playerCollision = false; + private int userId = -1; + private int memberId = -1; + private String userName = ""; + + public Player(int userId, int memberId) { + this.userId = userId; + this.memberId = memberId; + this.userName = Accounts.getInstance().getUserNameById(this.userId); + actor = createPlayerObject(null); + setInitDirection(0.0, 0.0, 0.0); + // TODO:初期位置を設定する + setInitPosition(Math.random()*100, Math.random()*100, 0); + } + + /** + * プレイヤーの情報を更新する + * + * @param position3D プレイヤーの位置 + * @param vector3d プレイヤーの向き + */ + public void update(Position3D position3D, Vector3d vector3d) { + actor.setPosition(position3D); // プレイヤーの新しい位置をセット + actor.setDirection(vector3d); // プレイヤーの新しい向いている方向をセット + } + + /** + * プレイヤーのオブジェクトを作成 + * + * @param animation3D アニメーション + * @return プレイヤーのオブジェクト + */ + private OvergroundActor createPlayerObject(Animation3D animation3D) { + // TODO:プレイヤーの見た目を考える + String path = Battles.getInstance().getClass().getResource("pocha.stl").getPath(); + try { + path = URLDecoder.decode(path, "utf-8"); + } catch (Exception e) { + + } + PlayerModel playerModel = new PlayerModel(path, null); + return new OvergroundActor(new Solid3D(playerModel.createObject()), null); + } + + public String getUserName() { + return userName; + } + + /** + * プレイヤーの初期位置を設定する + * + * @param x x座標 + * @param y y座標 + * @param z z座標 + */ + public void setInitPosition(double x, double y, double z){ + actor.setPosition(new Position3D(x, y, z)); + } + + /** + * プレイヤーの初期方向を設定する + * + * @param x x方向 + * @param y y方向 + * @param z z方向 + */ + public void setInitDirection(double x, double y, double z) { + actor.setDirection(new Vector3d(x, y, z)); + } + + /** + * プレイヤーの位置を取得 + * + * @return プレイヤーの位置 + */ + public Position3D getPlayerPosition3d() { + return actor.getPosition(); + } + + /** + * プレイヤーの向きを取得 + * + * @return プレイヤーの向き + */ + public Vector3d getPlayerVector3d() { + return actor.getDirection(); + } + + /** + * プレイヤーのHPを取得 + * + * @return プレイヤーのHP + */ + public int getHp() { + return hp; + } + + /** + * プレイヤーにダメージを与える + * + * @param damage 与えるダメージ + */ + public void setDamage(int damage) { + hp -= damage; + } + + /** + * プレイヤーが生存しているかどうか判定 + * + * @return 生きている:true, 死んでいる:false + */ + @JSONHint(ignore = true) + public boolean isAlive() { + if (hp < 0) { + return false; + } + return true; + } + + /** + * プレイヤー同士の衝突判定 + * + * @param player 判定するプレイヤー + * @return 衝突している時はtrue, していない時はfalse + */ + public boolean checkCollision(Player player) { + if (PhysicsUtility.checkCollision(getBody(), null, player.getBody(), null) != null) { + return true; + } + return false; + } + + public boolean isPlayerCollision() { + return playerCollision; + } + + /** + * プレイヤー同士の衝突フラグ + * + * @param playerCollision + */ + public void setPlayerCollision(boolean playerCollision) { + this.playerCollision = playerCollision; + } + + /** + * プレイヤーのオブジェクトを取得する + * + * @return プレイヤーのオブジェクト + */ + @JSONHint(ignore = true) + public Object3D getBody() { + return actor.body; + } + + /** + * プレイヤーのuserIdを取得する + * + * @return userId + */ + @JSONHint(ignore = true) + public int getUserId() { + return userId; + } + + /** + * プレイヤーのmemberIdを取得する + * + * @return memberId + */ + @JSONHint(ignore = true) + public int getMemberId() { + return memberId; + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/PlayerData.java b/src/main/java/org/ntlab/SproutServer/battles/PlayerData.java new file mode 100644 index 0000000..53a5e65 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/PlayerData.java @@ -0,0 +1,70 @@ +package org.ntlab.SproutServer.battles; + +import framework.model3D.Position3D; +import framework.physics.Velocity3D; + +import javax.vecmath.Vector3d; + +/** + * プレイヤー情報を更新するためのデータ + * + * @author matsumoto_k + */ +public class PlayerData { + + /* プレイヤーに関するデータ */ + private Position3D userPosition = null; // プレイヤーの位置 + private Vector3d userVector = null; // プレイヤー向き + + /* 武器に関するデータ */ + private Position3D weaponPosition = null; // 武器の位置 + private Vector3d weaponVector = null; // 武器の向き + private Velocity3D weaponVelocity = null; // 武器の速度 + + public PlayerData() { + } + + public boolean isWeaponShoot() { + return weaponPosition != null; + } + + public Position3D getUserPosition() { + return userPosition; + } + + public void setUserPosition(Position3D userPosition) { + this.userPosition = userPosition; + } + + public Vector3d getUserVector() { + return userVector; + } + + public void setUserVector(Vector3d userVector) { + this.userVector = userVector; + } + + public Position3D getWeaponPosition() { + return weaponPosition; + } + + public void setWeaponPosition(Position3D weaponPosition) { + this.weaponPosition = weaponPosition; + } + + public Vector3d getWeaponVector() { + return weaponVector; + } + + public void setWeaponVector(Vector3d weaponVector) { + this.weaponVector = weaponVector; + } + + public Velocity3D getWeaponVelocity() { + return weaponVelocity; + } + + public void setWeaponVelocity(Velocity3D weaponVelocity) { + this.weaponVelocity = weaponVelocity; + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/PlayerModel.java b/src/main/java/org/ntlab/SproutServer/battles/PlayerModel.java new file mode 100644 index 0000000..eff97bf --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/PlayerModel.java @@ -0,0 +1,24 @@ +package org.ntlab.SproutServer.battles; + +import framework.model3D.ModelFactory; +import framework.model3D.Object3D; + +public class PlayerModel { + String modelFileName = null; + String animationFileName = null; + + public PlayerModel(String modelFileName, String animationFileName) { + this.animationFileName = animationFileName; + this.modelFileName = modelFileName; + } + + public Object3D createObject() { + Object3D object3D = null; + try { + object3D = ModelFactory.loadModel(modelFileName, false, false).createObject(); + } catch (Exception e) { + + } + return object3D; + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/Role.java b/src/main/java/org/ntlab/SproutServer/battles/Role.java new file mode 100644 index 0000000..44e35c2 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/Role.java @@ -0,0 +1,24 @@ +package org.ntlab.SproutServer.battles; + +/** + * 職業に関するクラス + * @author matsumoto_k + */ +public enum Role { + Gunman(0), + Witch(1); + + private int id; + + private Role(int id) { + this.id = id; + } + + public static Role getRole(int id) { + for (Role role : values()) { + if (id == role.id) + return role; + } + return null; + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/StandardStage.java b/src/main/java/org/ntlab/SproutServer/battles/StandardStage.java new file mode 100644 index 0000000..41477d0 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/StandardStage.java @@ -0,0 +1,17 @@ +package org.ntlab.SproutServer.battles; + +import framework.model3D.ModelFactory; +import framework.physics.Ground; +import framework.physics.Solid3D; + +/** + * ステージ + * + * @author matsumoto_k + * TODO:ステージを決める + */ +public class StandardStage extends Ground { + public StandardStage(String filePath) { + super(new Solid3D(ModelFactory.loadModel(filePath, false, false).createObject())); + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/Team.java b/src/main/java/org/ntlab/SproutServer/battles/Team.java new file mode 100644 index 0000000..987379c --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/Team.java @@ -0,0 +1,264 @@ +package org.ntlab.SproutServer.battles; + +import framework.model3D.Placeable; +import framework.model3D.Position3D; +import framework.physics.PhysicsUtility; +import framework.physics.Velocity3D; +import net.arnx.jsonic.JSONHint; +import org.ntlab.SproutServer.rooms.Member; +import org.ntlab.SproutServer.rooms.Room; + +import javax.vecmath.Vector3d; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * チーム + * + * @author matsumoto_k + */ +public class Team { + private HashMap playerMap = new HashMap<>(); // チームのプレイヤー情報 + private HashMap bullets = new HashMap<>(); // チームの弾の情報 + private HashMap magics = new HashMap<>(); // チームの魔法の情報 + private int bulletId = 0; // 弾を判別するID + private int magicId = 0; + private boolean result; // 勝敗 + + public Team(Room room) { + playerMap = new HashMap<>(); + for (Member member : room.getMemberList().values()) { + /** + * e.getKey : userId + */ + playerMap.put(member.getUserId(), new Player(member.getUserId(), member.getMemberId())); + } + } + + public HashMap getPlayerMap() { + return playerMap; + } + + public void setPlayerMap(HashMap playerMap) { + this.playerMap = playerMap; + } + + public boolean isResult() { + return result; + } + + public void setResult(boolean result) { + this.result = result; + } + + public HashMap getBullets() { + return bullets; + } + + public HashMap getMagics() { + return magics; + } + + /** + * userIdによってプレイヤーを取得 + * + * @param userId 取得するプレイヤーのuserId + * @return プレイヤー + */ + public Player findPlayerById(int userId) { + return playerMap.get(userId); + } + + /** + * チーム内の生存者の数を取得する + * + * @return 生存者の数 + */ + @JSONHint(ignore = true) + public int getAliveCount() { + int count = 0; + for (Player player : playerMap.values()) { + if (player.isAlive()) + count++; + } + return count; + } + + /** + * チーム内のプレイヤーが全員死亡しているか取得 + * + * @return 全員死亡している:true, 一人でも生きている:false + */ + @JSONHint(ignore = true) + public boolean isAllPlayerDead() { + if (getAliveCount() == 0) + return true; + return false; + } + + /** + * 自クラスの武器と他のチームのプレイヤーの衝突したオブジェクトを取得する + * + * @param enemyTeam 判定させるチーム + * @return colliedObjects 衝突したオブジェクト + */ + public ArrayList getColliedObjects(Team enemyTeam) { + ArrayList colliedObjects = new ArrayList<>(); + + /* 弾 */ + for (Iterator bulletIterator = bullets.values().iterator(); bulletIterator.hasNext(); ) { + Bullet bullet = bulletIterator.next(); + + if (!bullet.isAlive()) { + colliedObjects.add(bullet.getActor()); + bulletIterator.remove(); + continue; + } + + for (Player player : enemyTeam.getPlayerMap().values()) { + /* 自チームの弾と他のチームのプレイヤーが衝突した時 */ + if (checkCollision(bullet, player)) { + /* 他のチームのプレイヤーにダメージを与える */ + player.setDamage(bullet.getAttack()); + /* 弾は取り除く */ + colliedObjects.add(bullet.getActor()); + bulletIterator.remove(); + } + } + } + + /* 魔法 */ + for (Iterator magicIterator = magics.values().iterator(); magicIterator.hasNext(); ) { + Magic magic = magicIterator.next(); + + if (!magic.isAlive()) { + colliedObjects.add(magic.getActor()); + magicIterator.remove(); + continue; + } + + for (Player player : enemyTeam.getPlayerMap().values()) { + /* 自チームの弾と他のチームのプレイヤーが衝突した時 */ + if (checkCollision(magic, player)) { + /* 他のチームのプレイヤーにダメージを与える */ + player.setDamage(magic.getAttack()); + /* 弾は取り除く */ + colliedObjects.add(magic.getActor()); + magicIterator.remove(); + } + } + } + + return colliedObjects; + } + + /** + * 自クラスの武器と他のチームのプレイヤーの衝突したオブジェクトを取得する + * + * @param enemyPlayer 敵プレイヤー + * @return 衝突したオブジェクト + */ + public ArrayList getColliedObjects(Player enemyPlayer) { + ArrayList obj = new ArrayList<>(); + + /* 弾 */ + for (Iterator bulletIterator = bullets.values().iterator(); bulletIterator.hasNext(); ) { + Bullet bullet = bulletIterator.next(); + + if (!bullet.isAlive()) { + obj.add(bullet.getActor()); + bulletIterator.remove(); + continue; + } + + if (checkCollision(bullet, enemyPlayer)) { + /* 他のチームのプレイヤーにダメージを与える */ + enemyPlayer.setDamage(bullet.getAttack()); + /* 弾は取り除く */ + obj.add(bullet.getActor()); + bulletIterator.remove(); + } + } + + /* 魔法 */ + for (Iterator magicIterator = magics.values().iterator(); magicIterator.hasNext(); ) { + Magic magic = magicIterator.next(); + + if (!magic.isAlive()) { + obj.add(magic.getActor()); + magicIterator.remove(); + continue; + } + + if (checkCollision(magic, enemyPlayer)) { + /* 他のチームのプレイヤーにダメージを与える */ + enemyPlayer.setDamage(magic.getAttack()); + /* 弾は取り除く */ + obj.add(magic.getActor()); + magicIterator.remove(); + } + } + return obj; + } + + /** + * プレイヤーと武器の衝突判定 + * + * @param weapon + * @param player 他のチームのプレイヤー + * @return + */ + public boolean checkCollision(Weapon weapon, Player player) { + return PhysicsUtility.checkCollision(weapon.getBody(), null, player.getBody(), null) != null; + } + + /** + * チーム内のプレイヤーとの衝突判定 + * + * @param player 判定するプレイヤー + * @param playerId 判定するプレイヤーid + * @return 衝突している時はtrue, していない時はfalse + */ + public boolean checkCollision(Player player, int playerId) { + for (Map.Entry entry : playerMap.entrySet()) { + /* idがplayerと違う時 */ + if (entry.getKey() != playerId) { + if (player.checkCollision(entry.getValue())) + return true; + } + } + return false; + } + + /** + * プレイヤーの弾を作成する + * + * @param position3D 弾の位置 + * @param vector3d 弾の向き + * @param velocity3D 弾の速度 + * @return + */ + public Bullet createBullet(Position3D position3D, Vector3d vector3d, Velocity3D velocity3D) { + Bullet bullet = new Bullet(position3D, vector3d, velocity3D); + bullets.put(bulletId, bullet); + bulletId++; + return bullet; + } + + /** + * プレイヤーの魔法を作成する + * + * @param position3D 魔法の位置 + * @param vector3d 魔法の向き + * @param velocity3D 魔法の速度 + * @return + */ + public Magic createMagic(Position3D position3D, Vector3d vector3d, Velocity3D velocity3D) { + Magic magic = new Magic(position3D, vector3d, velocity3D); + magics.put(magicId, magic); + magicId++; + return magic; + } +} diff --git a/src/main/java/org/ntlab/SproutServer/battles/Weapon.java b/src/main/java/org/ntlab/SproutServer/battles/Weapon.java new file mode 100644 index 0000000..5cb5eb4 --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/battles/Weapon.java @@ -0,0 +1,137 @@ +package org.ntlab.SproutServer.battles; + +import framework.animation.Animation3D; +import framework.gameMain.Actor; +import framework.model3D.CollisionResult; +import framework.model3D.Object3D; +import framework.model3D.Position3D; +import framework.physics.Force3D; +import framework.physics.Ground; +import framework.physics.Solid3D; +import framework.physics.Velocity3D; +import net.arnx.jsonic.JSONHint; + +import javax.vecmath.Vector3d; + +/** + * 飛ぶ武器 + * + * @author matsumoto_k + */ +abstract class Weapon { + + protected boolean alive = true; // 武器の生存フラグ + private long left = 0; // 武器の寿命 + private int attack = 0; // 武器の攻撃力 + protected Actor actor; // 武器のオブジェクト + + public Weapon() { + left = getInitWeaponLife(); + attack = getAttack(); + this.actor = new WeaponActor(new Solid3D(createObject()), null) { + public void onIntersect(CollisionResult normal, long interval) { + if (normal != null) + alive = false; + } + }; + } + + abstract protected class WeaponActor extends Actor { + protected WeaponActor(Solid3D o, Animation3D animation3D) { + super(o, animation3D); + } + + @Override + public void onEndFall() { + + } + + @Override + abstract public void onIntersect(CollisionResult normal, long interval); + + @Override + public void motion(long interval, Ground ground) { + super.motion(interval, ground); + left -= interval; + if (left < 0L) { + alive = false; + } + } + + @Override + public Force3D getGravity() { + return Force3D.ZERO; + } + + @Override + public void onEndAnimation() { + + } + } + + /** + * 武器のオブジェクトを作成する + * + * @return 武器のオブジェクト + */ + abstract Object3D createObject(); + + /** + * 武器の初期値を設定 + * + * @param weaponPosition 武器の位置 + * @param weaponVector 武器の方向 + * @param weaponVelocity 武器の速度 + */ + public void initWeapon(Position3D weaponPosition, Vector3d weaponVector, Velocity3D weaponVelocity) { + actor.setPosition(weaponPosition); // 弾の位置を設定 + actor.setDirection(weaponVector); // 弾の方向を設定 + actor.setVelocity(weaponVelocity); // 弾の速度を設定 + } + + /** + * 武器のActorを取得 + * + * @return 武器のActor + */ + @JSONHint(ignore = true) + public Actor getActor() { + return actor; + } + + /** + * 武器のオブジェクトを取得 + * + * @return 武器のオブジェクト + */ + @JSONHint(ignore = true) + public Object3D getBody() { + return actor.body; + } + + /** + * 武器の生存確認 + * + * @return 武器が壁や地面と接触した、武器の寿命が切れたなどの場合はfalseを返す + */ + @JSONHint(ignore = true) + public boolean isAlive() { + return alive; + } + + /** + * 武器の初期寿命を取得 + * + * @return 武器の初期寿命(ミリ秒) + */ + @JSONHint(ignore = true) + abstract long getInitWeaponLife(); + + /** + * 武器の攻撃力を取得 + * + * @return 武器の攻撃力 + */ + @JSONHint(ignore = true) + abstract public int getAttack(); +} diff --git a/src/main/java/org/ntlab/SproutServer/rooms/Member.java b/src/main/java/org/ntlab/SproutServer/rooms/Member.java new file mode 100644 index 0000000..c634fce --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/rooms/Member.java @@ -0,0 +1,102 @@ +package org.ntlab.SproutServer.rooms; +import java.util.ArrayList; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.ntlab.SproutServer.accounts.Account; +import org.ntlab.SproutServer.accounts.Accounts; + +import net.arnx.jsonic.JSONHint; + +/** + * Root resource (exposed at "Rooms" path) + */ + +public class Member { + + /** + * アカウントリストからユーザ情報の検索 + */ + public Member(int userId) { + Accounts ac = Accounts.getInstance(); + for(int i=0; i memberList = new HashMap(); + public boolean readyToFight = false; //全員準備完了したか判定 + private int memberId=0; + public boolean startFrag; + public boolean battleState = false; + public int battleId = -1; + + + public Room(String roomName,int roomId,String roomKey,Member member){ + this.roomId = roomId; + this.roomName = roomName; + this.roomKey = roomKey; + this.memberList.put(this.memberId,member); + memberList.get(this.memberId).setMemberId(this.memberId); + this.memberList.get(memberId).setHostState(true); + this.hostName = member.getUserName(); + this.memberId++; + + if(roomKey != null){ + this.keyCheck = true; + } + + } + + public Room(int roomId){ + this.roomId = roomId; + } + + public Room() { + // TODO Auto-generated constructor stub + } + + + /** + * 入室処理 + */ + public void enterRoom(int memberId,Member member){ + getMemberList().put(memberId, member); + this.memberList.get(this.memberId).setMemberId(this.memberId); + this.memberId++; + } + + /** + * パスワードのチェック + */ + public boolean keyCheck(String key,int memberId){ + if(this.keyCheck == false){ + return true; + } + + boolean counts = false; + if(key.equals(this.getRoomKey())){ + counts = true; + } + System.out.println("key chek = " + counts); + return counts; + } + + + /** + * バトル終了時のルームの状態の初期化 + */ + public void roomInitialize(){ + this.battleState = false; + this.readyToFight = false; + this.startFrag = false; + this.battleId = -1; + for(int value : this.memberList.keySet()){ + if(this.memberList.get(value) != null){ + this.memberList.get(value).setReady(false); + } + } + } + + + /** + * ホストの次に入った人のメンバーIDを返す + */ + public int minimumMemberCount(int memberId){ + int count = 0; + for(int i = 0;i getMemberList() { + return memberList; + } + + public void setMemberList(HashMap memberList) { + this.memberList = memberList; + } + + + //ルームID + @JSONHint(ignore=true) + public int getRoomId() { + return roomId; + } + + public void setId(int id) { + this.roomId = id; + } + + + //ルーム名 + @JSONHint(ignore=true) + public String getRoomName() { + return roomName; + } + + public void setRoomName(String roomName) { + this.roomName = roomName; + } + + + //ホストネーム + @JSONHint(ignore=true) + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + + //キーのチェック + @JSONHint(ignore=true) + public boolean isKeyCheck() { + return keyCheck; + } + + public void setKeyCheck(boolean keyCheck) { + this.keyCheck = keyCheck; + } + + + //準備完了判定 + @JSONHint(ignore=true) + public boolean isReadyToFight() { + return readyToFight; + } + + public void setReadyToFight(boolean allReady) { + this.readyToFight = allReady; + } + + + //ルームキー(ルームパスワード) + @JSONHint(ignore=true) + public String getRoomKey() { + return roomKey; + } + + public void setRoomKey(String roomKey) { + this.roomKey = roomKey; + } + + //メンバーID(入った順番) + @JSONHint(ignore=true) + public int getMemberId() { + return memberId; + } + + public void setMemberId(int memberId) { + this.memberId = memberId; + } + + //全員準備 + public boolean isStartFrag() { + return startFrag; + } + + public void setStartFrag(boolean startFrag) { + this.startFrag = startFrag; + } + + //バトル中か否か + public boolean isBattleState() { + return battleState; + } + + public void setBattleState(boolean battleState) { + this.battleState = battleState; + } + + + //バトルID + public int getBattleId() { + return battleId; + } + + public void setBattleId(int battleId) { + this.battleId = battleId; + } + + + + + + + + + + + + + + + +} diff --git a/src/main/java/org/ntlab/SproutServer/rooms/RoomResponse.java b/src/main/java/org/ntlab/SproutServer/rooms/RoomResponse.java new file mode 100644 index 0000000..ce16feb --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/rooms/RoomResponse.java @@ -0,0 +1,60 @@ +package org.ntlab.SproutServer.rooms; + +public class RoomResponse { + + public Room room; + public int memberId; + public boolean check; + public int battleId; + + public RoomResponse(Room room,int userId,int memberId,boolean check){ + this.room = room; + this.check = check; + this.memberId = memberId; + } + + public RoomResponse(Room room,int userId){ + this.room = room; + this.battleId = room.getBattleId(); + for(int i = 0;i roomList = new HashMap(); + public int roomId = 0; // ルームID + private Room waitingRoom = null; //待ち部屋 + private Room waitingRoomSecond = null; //待ち部屋2 + + + + + private Rooms(){ + if (theInstance == null) { + theInstance = this; + } + } + + public static Rooms getInstance() { + if (theInstance == null) { + theInstance = new Rooms(); + } + return theInstance; + } + + + + /** + * 部屋のメンバーの準備チェック + */ + public int readyCheck(int roomId,boolean frag){ + int checkCount = 0; + int battleCount = 0; + Room room = roomList.get(roomId); + + if(frag == true){ + for(int i = 0;i getRoomList() { + return roomList; + } + + +// public void setRoomList(HashMap roomList) { +// Rooms.roomList = roomList; +// } + + + //待ち部屋1 + //@JSONHint(ignore=true) + public Room getWaitingRoom() { + return waitingRoom; + } + + public void setWaitingRoom(Room waitingRoom) { + this.waitingRoom = waitingRoom; + } + + //待ち部屋2 + //@JSONHint(ignore=true) + public Room getWaitingRoomSecond() { + return waitingRoomSecond; + } + + public void setWaitingRoomSecond(Room waitingRoomSecond) { + this.waitingRoomSecond = waitingRoomSecond; + } + + //人数 + @JSONHint(ignore=true) + public int getPlayerNumber() { + return playerNumber; + } + + + //ルームID + public int getRoomId() { + return roomId; + } + + public void setRoomId(int roomId) { + this.roomId = roomId; + } + + +} diff --git a/src/main/java/org/ntlab/SproutServer/rooms/RoomsRest.java b/src/main/java/org/ntlab/SproutServer/rooms/RoomsRest.java new file mode 100644 index 0000000..0202eda --- /dev/null +++ b/src/main/java/org/ntlab/SproutServer/rooms/RoomsRest.java @@ -0,0 +1,216 @@ +package org.ntlab.SproutServer.rooms; + +import org.ntlab.SproutServer.battles.Battles; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import java.util.HashMap; + +@Path("rooms") +public class RoomsRest { + + Rooms rooms; + /** + * 部屋一覧取得(Rooms) + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public Rooms roomsView() { + return rooms.getInstance(); + } + + + /** + * 部屋の状態(Room) + */ + @GET + @Path("/{roomId}") + @Produces(MediaType.APPLICATION_JSON) + public Room roomState(@PathParam("roomId") int roomId){ + return rooms.getRoomList().get(roomId); + } + + + /** + * 他アカウント情報取得(member) + */ + @GET + @Path("/{roomId}/{memberId}") + @Produces(MediaType.APPLICATION_JSON) + public HashMap getOtherAccount(@PathParam("roomId") int roomId, @QueryParam("userId") int userId) { + return rooms.getRoomList().get(roomId).getMemberList(); + } + + + /** + * 部屋作成(Rooms) + */ + @POST + @Produces(MediaType.APPLICATION_JSON) + public RoomResponse makeRoom(@FormParam("userId") int userId, @FormParam("roomName") String roomName, @FormParam("key") String key) { + int roomId = rooms.getRoomId(); + Member member = new Member(userId); + + Room room = new Room(roomName,rooms.getRoomId(),key,member); + rooms.getRoomList().put(rooms.getRoomId(),room); + + RoomResponse roomRes = new RoomResponse(room, userId); + + roomId++; + rooms.setRoomId(roomId); + + return roomRes; + } + + + /** + * 入室(Room) + */ + @PUT + @Path("/{roomId}") + @Produces(MediaType.APPLICATION_JSON) + public RoomResponse enterRoom(@PathParam("roomId") int roomId,@FormParam("userId") int userId,@FormParam("key") String key){ + Room room = rooms.getRoomList().get(roomId); + RoomResponse roomRes; + int memberId = room.getMemberId(); + boolean check = room.keyCheck(key, memberId); + boolean keyCheck = true; + + if(check == true && room.getMemberList().size() + + + + Jersey Web Application + org.glassfish.jersey.servlet.ServletContainer + + jersey.config.server.provider.packages + org.ntlab.SproutServer + + 1 + + + Jersey Web Application + /* + +