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
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
|
page.title=Backward Compatibility for Applications
@jd:body
<p>A variety of Android-powered devices are now available to consumers from carriers
in geographies around the world. Across those devices, a range of Android
platform versions are in use, some running the latest version of the platform,
others running older versions. As a developer, you need to consider the approach
to backward compatibility that you will take in your application — do you
want to allow your application to run on all devices, or just those running the
latest software? In some cases it will be useful to employ the newer APIs on
devices that support them, while continuing to support older devices. </p>
<h3>Setting the minSdkVersion</h3>
<p>If the use of a new API is integral to the application — perhaps you
need to record video using an API introduced in Android 1.5 (API Level 3)
— you should add a <a
href="{@docRoot}guide/topics/manifest/uses-sdk-element.html"><code><android:minSdkVersion></code></a>
to the application's manifest, to ensure your app won't
be installed on older devices. For example, if your application depends on an
API introduced in API Level 3, you would specify "3" as the value of the minimum
SDK version</a>:</p>
<pre> <manifest>
...
<uses-sdk android:minSdkVersion="3" />
...
</manifest></pre>
<p>However, if you want to add a useful but non-essential feature, such as
popping up an on-screen keyboard even when a hardware keyboard is available, you
can write your program in a way that allows it to use the newer features without
failing on older devices.</p>
<h3>Using reflection</h3>
<p>Suppose there's a simple new call you want to use, like {@link
android.os.Debug#dumpHprofData(java.lang.String)
android.os.Debug.dumpHprofData(String filename)}. The {@link android.os.Debug}
class has existed since Android 1.0, but the method is new in Anroid 1.5 (API
Level 3). If you try to call it directly, your app will fail to run on devices
running Android 1.1 or earlier.</p>
<p>The simplest way to call the method is through reflection. This requires
doing a one-time lookup and caching the result in a <code>Method</code> object.
Using the method is a matter of calling <code>Method.invoke</code> and un-boxing
the result. Consider the following:</p>
<pre>public class Reflect {
private static Method mDebug_dumpHprofData;
static {
initCompatibility();
};
private static void initCompatibility() {
try {
mDebug_dumpHprofData = Debug.class.getMethod(
"dumpHprofData", new Class[] { String.class } );
/* success, this is a newer device */
} catch (NoSuchMethodException nsme) {
/* failure, must be older device */
}
}
private static void dumpHprofData(String fileName) throws IOException {
try {
mDebug_dumpHprofData.invoke(null, fileName);
} catch (InvocationTargetException ite) {
/* unpack original exception when possible */
Throwable cause = ite.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if (cause instanceof Error) {
throw (Error) cause;
} else {
/* unexpected checked exception; wrap and re-throw */
throw new RuntimeException(ite);
}
} catch (IllegalAccessException ie) {
System.err.println("unexpected " + ie);
}
}
public void fiddle() {
if (mDebug_dumpHprofData != null) {
/* feature is supported */
try {
dumpHprofData("/sdcard/dump.hprof");
} catch (IOException ie) {
System.err.println("dump failed!");
}
} else {
/* feature not supported, do something else */
System.out.println("dump not supported");
}
}
}</pre>
<p>This uses a static initializer to call <code>initCompatibility</code>,
which does the method lookup. If that succeeds, it uses a private
method with the same semantics as the original (arguments, return
value, checked exceptions) to do the call. The return value (if it had
one) and exception are unpacked and returned in a way that mimics the
original. The <code>fiddle</code> method demonstrates how the
application logic would choose to call the new API or do something
different based on the presence of the new method.</p>
<p>For each additional method you want to call, you would add an additional
private <code>Method</code> field, field initializer, and call wrapper to the
class.</p>
<p>This approach becomes a bit more complex when the method is declared in a
previously undefined class. It's also much slower to call
<code>Method.invoke()</code> than it is to call the method directly. These
issues can be mitigated by using a wrapper class.</p>
<h3>Using a wrapper class</h3>
<p>The idea is to create a class that wraps all of the new APIs exposed by a new
or existing class. Each method in the wrapper class just calls through to the
corresponding real method and returns the same result.</p>
<p>If the target class and method exist, you get the same behavior you would get
by calling the class directly, with a small amount of overhead from the
additional method call. If the target class or method doesn't exist, the
initialization of the wrapper class fails, and your application knows that it
should avoid using the newer calls.</p>
<p>Suppose this new class were added:</p><pre>public class NewClass {
private static int mDiv = 1;
private int mMult;
public static void setGlobalDiv(int div) {
mDiv = div;
}
public NewClass(int mult) {
mMult = mult;
}
public int doStuff(int val) {
return (val * mMult) / mDiv;
}
}</pre>
<p>We would create a wrapper class for it:</p>
<pre>class WrapNewClass {
private NewClass mInstance;
/* class initialization fails when this throws an exception */
static {
try {
Class.forName("NewClass");
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
/* calling here forces class initialization */
public static void checkAvailable() {}
public static void setGlobalDiv(int div) {
NewClass.setGlobalDiv(div);
}
public WrapNewClass(int mult) {
mInstance = new NewClass(mult);
}
public int doStuff(int val) {
return mInstance.doStuff(val);
}
}</pre>
<p>This has one method for each constructor and method in the original, plus a
static initializer that tests for the presence of the new class. If the new
class isn't available, initialization of <code>WrapNewClass</code> fails,
ensuring that the wrapper class can't be used inadvertently. The
<code>checkAvailable</code> method is used as a simple way to force class
initialization. We use it like this:</p>
<pre>public class MyApp {
private static boolean mNewClassAvailable;
/* establish whether the "new" class is available to us */
static {
try {
WrapNewClass.checkAvailable();
mNewClassAvailable = true;
} catch (Throwable t) {
mNewClassAvailable = false;
}
}
public void diddle() {
if (mNewClassAvailable) {
WrapNewClass.setGlobalDiv(4);
WrapNewClass wnc = new WrapNewClass(40);
System.out.println("newer API is available - " + wnc.doStuff(10));
} else {
System.out.println("newer API not available");
}
}
}</pre>
<p>If the call to <code>checkAvailable</code> succeeds, we know the new class is
part of the system. If it fails, we know the class isn't there, and adjust our
expectations accordingly. It should be noted that the call to
<code>checkAvailable</code> will fail before it even starts if the bytecode
verifier decides that it doesn't want to accept a class that has references to a
nonexistent class. The way this code is structured, the end result is the same
whether the exception comes from the verifier or from the call to
<code>Class.forName</code>.</p>
<p>When wrapping an existing class that now has new methods, you only need to
put the new methods in the wrapper class. Invoke the old methods directly. The
static initializer in <code>WrapNewClass</code> would be augmented to do a
one-time check with reflection.</p>
<h3>Testing is key</h3>
<p>You must test your application on every version of the Android framework that
is expected to support it. By definition, the behavior of your application will
be different on each. Remember the mantra: if you haven't tried it, it doesn't
work.</p>
<p>You can test for backward compatibility by running your application in an
emulator that uses an older version of the platform. The Android SDK allows you
to do this easily by creating "Android Virtual Devices" with different API
levels. Once you create the AVDs, you can test your application with old and new
versions of the system, perhaps running them side-by-side to see the
differences. More information about emulator AVDs can be found <a
href={@docRoot}guide/developing/tools/avd.html">in the AVD documentation</a> and
from <code>emulator -help-virtual-device</code>.</p>
|