Android: View.setID(int id) programmatically - how to avoid ID conflicts? - android

I'm adding TextViews programmatically in a for-loop and add them to an ArrayList.
How do I use TextView.setId(int id)? What Integer ID do I come up with so it doesn't conflict with other IDs?

From API level 17 and above, you can call: View.generateViewId()
Then use View.setId(int).
If your app is targeted lower than API level 17, use ViewCompat.generateViewId()

You can define the ID's you'll use later in R.id class using an xml resource file, and let Android SDK set the actual unique values during compile time.
res/values/ids.xml
<item name="my_edit_text_1" type="id"/>
<item name="my_button_1" type="id"/>
<item name="my_time_picker_1" type="id"/>
To use it in the code:
myEditTextView.setId(R.id.my_edit_text_1);

According to View documentation
The identifier does not have to be unique in this view's hierarchy. The identifier should be a positive number.
So you can use any positive integer you like, but in this case there can be some views with equivalent id's. If you want to search for some view in hierarchy calling to setTag with some key objects may be handy.

Also you can define ids.xml in res/values. You can see an exact example in android's sample code.
samples/ApiDemos/src/com/example/android/apis/RadioGroup1.java
samples/ApiDemp/res/values/ids.xml

Since API 17, the View class has a static method generateViewId() that will
generate a value suitable for use in setId(int)

This works for me:
static int id = 1;
// Returns a valid id that isn't in use
public int findId(){
View v = findViewById(id);
while (v != null){
v = findViewById(++id);
}
return id++;
}

(This was a comment to dilettante's answer but it got too long...hehe)
Of course a static is not needed here. You could use SharedPreferences to save, instead of static. Either way, the reason is to save the current progress so that its not too slow for complicated layouts. Because, in fact, after its used once, it will be rather fast later. However, I dont feel this is a good way to do it because if you have to rebuild your screen again (say onCreate gets called again), then you probably want to start over from the beginning anyhow, eliminating the need for static. Therefore, just make it an instance variable instead of static.
Here is a smaller version that runs a bit faster and might be easier to read:
int fID = 0;
public int findUnusedId() {
while( findViewById(++fID) != null );
return fID;
}
This above function should be sufficient. Because, as far as I can tell, android-generated IDs are in the billions, so this will probably return 1 the first time and always be quite fast.
Because, it wont actually be looping past the used IDs to find an unused one. However, the loop is there should it actually find a used ID.
However, if you still want the progress saved between subsequent recreations of your app, and want to avoid using static. Here is the SharedPreferences version:
SharedPreferences sp = getSharedPreferences("your_pref_name", MODE_PRIVATE);
public int findUnusedId() {
int fID = sp.getInt("find_unused_id", 0);
while( findViewById(++fID) != null );
SharedPreferences.Editor spe = sp.edit();
spe.putInt("find_unused_id", fID);
spe.commit();
return fID;
}
This answer to a similar question should tell you everything you need to know about IDs with android: https://stackoverflow.com/a/13241629/693927
EDIT/FIX: Just realized I totally goofed up the save. I must have been drunk.

The 'Compat' library now also supports the generateViewId() method for API levels prior 17.
Just make sure to use a version of the Compat library that is 27.1.0+
For example, in your build.gradle file, put :
implementation 'com.android.support:appcompat-v7:27.1.1
Then you can simply use the generateViewId() from the ViewCompat class instead of the View class as follow:
//Will assign a unique ID
myView.id = ViewCompat.generateViewId()
Happy coding !

Just an addition to the answer of #phantomlimb,
while View.generateViewId() require API Level >= 17,
this tool is compatibe with all API.
according to current API Level,
it decide weather using system API or not.
so you can use ViewIdGenerator.generateViewId() and View.generateViewId() in the
same time and don't worry about getting same id
import java.util.concurrent.atomic.AtomicInteger;
import android.annotation.SuppressLint;
import android.os.Build;
import android.view.View;
/**
* {#link View#generateViewId()}要求API Level >= 17,而本工具类可兼容所有API Level
* <p>
* 自动判断当前API Level,并优先调用{#link View#generateViewId()},即使本工具类与{#link View#generateViewId()}
* 混用,也能保证生成的Id唯一
* <p>
* =============
* <p>
* while {#link View#generateViewId()} require API Level >= 17, this tool is compatibe with all API.
* <p>
* according to current API Level, it decide weather using system API or not.<br>
* so you can use {#link ViewIdGenerator#generateViewId()} and {#link View#generateViewId()} in the
* same time and don't worry about getting same id
*
* #author fantouchx#gmail.com
*/
public class ViewIdGenerator {
private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
#SuppressLint("NewApi")
public static int generateViewId() {
if (Build.VERSION.SDK_INT < 17) {
for (;;) {
final int result = sNextGeneratedId.get();
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
int newValue = result + 1;
if (newValue > 0x00FFFFFF)
newValue = 1; // Roll over to 1, not 0.
if (sNextGeneratedId.compareAndSet(result, newValue)) {
return result;
}
}
} else {
return View.generateViewId();
}
}
}

In order to dynamically generate View Id form API 17 use
generateViewId()
Which will generate a value suitable for use in setId(int). This value will not collide with ID values generated at build time by aapt for R.id.

int fID;
do {
fID = Tools.generateViewId();
} while (findViewById(fID) != null);
view.setId(fID);
...
public class Tools {
private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
public static int generateViewId() {
if (Build.VERSION.SDK_INT < 17) {
for (;;) {
final int result = sNextGeneratedId.get();
int newValue = result + 1;
if (newValue > 0x00FFFFFF)
newValue = 1; // Roll over to 1, not 0.
if (sNextGeneratedId.compareAndSet(result, newValue)) {
return result;
}
}
} else {
return View.generateViewId();
}
}
}

inspired by #dilettante answer, here's my solution as an extension function in kotlin:
/* sets a valid id that isn't in use */
fun View.findAndSetFirstValidId() {
var i: Int
do {
i = Random.nextInt()
} while (findViewById<View>(i) != null)
id = i
}

public String TAG() {
return this.getClass().getSimpleName();
}
private AtomicInteger lastFldId = null;
public int generateViewId(){
if(lastFldId == null) {
int maxFld = 0;
String fldName = "";
Field[] flds = R.id.class.getDeclaredFields();
R.id inst = new R.id();
for (int i = 0; i < flds.length; i++) {
Field fld = flds[i];
try {
int value = fld.getInt(inst);
if (value > maxFld) {
maxFld = value;
fldName = fld.getName();
}
} catch (IllegalAccessException e) {
Log.e(TAG(), "error getting value for \'"+ fld.getName() + "\' " + e.toString());
}
}
Log.d(TAG(), "maxId="+maxFld +" name="+fldName);
lastFldId = new AtomicInteger(maxFld);
}
return lastFldId.addAndGet(1);
}

My Choice:
// Method that could us an unique id
int getUniqueId(){
return (int)
SystemClock.currentThreadTimeMillis();
}

Related

Insert int type variable from EditText into SQL Database in Android

I got an EditText and I want to insert an int type variable into my database. If I insert a number, everything's ok. But if I left the EditText empty, the app crashes. This is the code that handles this and I think it's an Integer.toString() problem or something like this.
int target;
if (targetNumber.getText().toString().isEmpty()) {
target = Integer.parseInt("");
} else {
target = Integer.parseInt(targetNumber.getText().toString());
}
You can't parse "" to int. You can store 0 for example
int target;
if (targetNumber.getText().toString().isEmpty()) {
target = 0;
} else {
target = Integer.parseInt(targetNumber.getText().toString());
}
Replace this
target = Integer.parseInt("");
with
target = 0;
If you'd like to make use of a ternary operator:
int target = targetNumber.getText().toString().isEmpty() ? 0 : Integer.parseInt(targetNumber.getText().toString())

How to apply #IntRange() support annotation to Kotlin property setter

I have been trying to find out how to apply #IntRange(from = 1) to my Kotlin property. After several failed attempts I finally just created the class I wanted in Java and converted it to Kotlin inside Android Studio. Here is my Java class:
import android.support.annotation.IntRange;
public class SimpleThing {
private int val;
#IntRange(from = 1)
public int getVal() {
return val;
}
public void setVal(#IntRange(from = 1) int val) {
this.val = val;
}
}
and this is the automatic conversion I got from Android Studio:
import android.support.annotation.IntRange
class SimpleThing {
#get:IntRange(from = 1)
var `val`: Int = 0
}
The #IntRange appears to get applied to the getter but not to the setter. Is it possible to apply this annotation to the setter also so that the appropriate lint warning will appear. Currently I have just overridden the set method to throw an IllegalArgumentException like this:
#get:IntRange(from = 1)
var rowSize: Int = 3
set(value) {
if (value < 1) throw IllegalArgumentException("row size must be at least 1")
field = value
notifyDataSetChanged()
}
I have already tried adding #set:IntRange(from = 1) but I get the error This annotation does not apply for type void because it is trying to apply #IntRange to the return value (which is void in the case of the setter) as opposed to the setter argument.
The #setparam annotation appears to be what I'm looking for, but no lint warning is raised in Android Studio when I try to assign a value outside the range.
Here is my new code:
#get:IntRange(from = 1)
#setparam:IntRange(from = 1)
var rowSize: Int = 3
set(value) {
if (value < 1) throw IllegalArgumentException("row size must be at least 1")
field = value
notifyDataSetChanged()
}
and here is where I would normally expect to see a lint warning telling me I am assigning a value outside the int range
However the lint warning does appear when using the SimpleThing class in Kotlin code
The lint warning also does not appear when calling the Kotlin method as Java code
It would appear that this functionality is just not implemented yet.
Not in the position to test this currently, but have you tried adding a #set like so:
#set:IntRange(from = 1)
#get:IntRange(from = 1)
var `val`: Int = 0

payUmoney integration is giving an error

After logging in, it's generating a hash value, but still giving error "Some problem occurred! try again".
PayUmoneySdkInitilizer.PaymentParam.Builder builder =
new PayUmoneySdkInitilizer.PaymentParam.Builder();
builder.setAmount(10.0)
.setTnxId("0nf7" + System.currentTimeMillis())
.setPhone(<My phone>)
.setProductName("product_name")
.setFirstName(<My Name>)
.setEmail(<My email>)
.setsUrl("https://www.payumoney.com/mobileapp/payumoney/success.php")
.setfUrl("https://www.payumoney.com/mobileapp/payumoney/failure.php")
.setUdf1("").setUdf2("").setUdf3("").setUdf4("").setUdf5("")
.setIsDebug(false)
.setKey(<mykey>)
.setMerchantId(<my debug merchant id>);
String tnxId="0nf7" + System.currentTimeMillis();
PayUmoneySdkInitilizer.PaymentParam paymentParam = builder.build();
String hashSequence = "<...>|"+tnxId+"|10.0|product_name|<My name>|<My email>|||||||||||salt";
String serverCalculatedHash= hashCal("SHA-512", hashSequence);
Toast.makeText(getApplicationContext(),
serverCalculatedHash, Toast.LENGTH_SHORT).show();
paymentParam.setMerchantHash(serverCalculatedHash);
// calculateServerSideHashAndInitiatePayment(paymentParam);
PayUmoneySdkInitilizer.startPaymentActivityForResult(TrayActivity.this, paymentParam);
public static String hashCal(String type, String str) {
byte[] hashseq = str.getBytes();
StringBuffer hexString = new StringBuffer();
try {
MessageDigest algorithm = MessageDigest.getInstance(type);
algorithm.reset();
algorithm.update(hashseq);
byte messageDigest[] = algorithm.digest();
for (int i = 0; i<messageDigest.length; i++) {
String hex = Integer.toHexString(0xFF &messageDigest[i]);
if (hex.length() == 1) { hexString.append("0"); }
hexString.append(hex);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} return hexString.toString();
}
You use in the code:
.setTnxId("0nf7" + System.currentTimeMillis())
And then later:
String tnxId="0nf7" + System.currentTimeMillis();
Probably not the only problem, but do you really want to use two different values for these (the time may change between the two calls)? Didn't you want the same tnxId in both cases?
TransactionIdProvider.java:
import java.util.Locale;
public class TransactionIdProvider {
private final static String DEFAULT_PREFIX = "ID";
// Convenient prime number for incrementing the counter
private final static long ID_ADD = 0xF0AD; // "f*ck off and die"
// 64b counter with non-trivial start value
private static long idCounter = 0x0101F00DDEADBEEFL;
/**
* Returns ID consisting of prefix string and 64b counter interleaved
* with 32b per-4s-timestamp.
*
* May produce identical ID (collision) when:
* 1) class is reloaded within 4s
* (to fix: serialize "idCounter" upon shutdown/restart of VM, or
* modify prefix per start of VM)
* 2) more than 2^64 IDs are requested within 4s (no fix, unexpected)
* 3) more than 2^64 IDs are requested after cca. 550 years.
* (no fix, unexpected)
* 4) more than one static instance of TransactionIdProvider is used
* (two or more VMs running the app) (to fix put different prefix in
* every VM/server running this)
*
* Length of returned ID is prefix.length() + 24 alphanumeric symbols.
*/
public static synchronized String getNewId(final String prefix) {
idCounter += ID_ADD; // increment counter
// get 32b timestamp per ~4s (millis/4096) (good for ~550 years)
final int timeStamp = (int)(System.currentTimeMillis()>>12);
final int idPart1 = (int)(idCounter>>32);
final int idPart2 = (int)(idCounter);
return String.format(Locale.US, "%s%08X%08X%08X",
prefix, idPart1, timeStamp, idPart2);
}
public static String getNewId() {
return getNewId(DEFAULT_PREFIX);
}
}
Not sure how much usable is this one, and if the ID may be so long. Feel free to use/modify it any way you wish.
Also I wonder, whether I didn't forget about something important, but can't recall anything.
The security aspect of this one is still quite weak, as within 4s time span the ID will be like simple addition, but at least it's not producing 1, 2, 3... series.
Did found some SDK docs, looks like txnId may be 25 chars long, so you have 1 char for prefix only. Or cut down on timestamp, using %07X in format and masking value with 0x0FFFFFFF, that would make it repeat every ~34 years -> 2 letters for prefix. Or change counter to 32b int, should be still more than enough, unless you expect thousands of transactions per second -> that would remove 8 chars. Or base32/base64 the whole ID to shorten it (depends what alphabet is legal for content)...
Or whatever... already spent enough time with this. Hire a pro.

setBackgroundColor does not work after Proguard

I am working on finishing up a project, and it's my first time using Proguard. I have a method to set background colors for the activity itself as well as some buttons. Before Proguard everything works fine. After Proguard the colors aren't set.
Before:
public void setBackgroundColor(String color, View background){
String id = "1";
try {
ColorId myObject = new ColorId();
Method method = ColorId.class.getMethod(color);
id = (String) method.invoke(myObject);
} catch (Exception e) {
e.printStackTrace();
id = "1";
}
int thisColor = Integer.valueOf(id);
switch(thisColor) {
case 0://black
background.setBackgroundColor(0xff000000);
break;
case 1://white
background.setBackgroundColor(0xffffffff);
break;
case 2://red
background.setBackgroundColor(0xffCC0000);
break;
...
default:
background.setBackgroundColor(0xff0099cc);
break;
}
}
After:
public void a(String paramString, View paramView){
try {
c localc = new c();
str = (String)c.class.getMethod(paramString, new Class[0]).invoke(localc, new Object[0]);
switch (Integer.valueOf(str).intValue()){
default:
paramView.setBackgroundColor(-16737844);
return;
}
}catch (Exception localException){
for (;;){
localException.printStackTrace();
String str = "1";
}
paramView.setBackgroundColor(-16777216);
return;
}
paramView.setBackgroundColor(-1);
return;
paramView.setBackgroundColor(-3407872);
return;
paramView.setBackgroundColor(-16737844);
return;
paramView.setBackgroundColor(-8355712);
return;
paramView.setBackgroundColor(-6697984);
return;
paramView.setBackgroundColor(-17613);
return;
paramView.setBackgroundColor(-5609780);
return;
paramView.setBackgroundColor(-35700);
}
Can anyone help explain what is happening here, and how I can make this method (and others in the future) work again after obfuscation? To me it looks like Proguard is rearranging things in regards to the switch.
Proguard shortens code by renaming classes and methods to have shorter names and by removing code that isn't referred to. Your code doesn't work because Proguard renamed or removed the ColorId methods black(), white(), and red(). To use reflection, you'd need to add Proguard keep directives to tell it to keep these methods and to keep their original names.
I don't have an explanation for why the "after" code's switch statement is messed up. Are you sure you decompiled it properly?
Why is the "before" code so convoluted? It uses reflection to look up a method by color name, then calls it to translate the color name to a String, parses the String to get an integer code, boxes the integer code into an Integer, unboxes it, uses a switch statement to pick a color value, then sets the background color, replicating the background.setBackgroundColor() call in each of the switch branches (breaking the DRY principle).
Reflection is an extreme tool to use in special cases like dynamically loaded code.
It'd be simpler, faster, and clearer to look up the color name in a HashMap:
static final int DEFAULT_COLOR = 0xff0099cc;
static final Map<String, Integer> colors = new HashMap<String, Integer>();
static {
colors.put("black", 0xff000000);
colors.put("white", 0xffffffff);
colors.put("red", 0xffCC0000);
}
public void setBackgroundColor(String color, View view) {
Integer colorInteger = colors.get(color);
int colorValue = colorInteger == null ? DEFAULT_COLOR : colorInteger.intValue();
view.setBackgroundColor(colorValue);
}
This HashMap is a good choice if the color has to be passed in as a string name. But if you can change the color argument, an enum would be more type safe, simpler, and faster:
public enum Color {
BLACK(0xff000000), WHITE(0xffffffff), RED(0xffCC0000), DEFAULT(0xff0099cc);
final int value;
Color(int value) { this.value = value; }
}
public void setBackgroundColor(Color color, View view) {
view.setBackgroundColor(color.value);
}
[It's better to define all your color values in an Android resource file (colors.xml). You can look them up by resource ID number.]
What is your proguard setup? Post your proguard-project.txt file (or proguard.cfg if you're using the old method). I'd suggest turning off obfuscation to see more clearly how the code is being changed. Use '-dontobfuscate'.
The 'after' code looks odd. Are you using the optimization common configuration file (proguard-android-optimize.txt)? If so, try to use without optimization to reduce how much your code is being modified.

Programmatic Views how to set unique id's?

I am creating in my app bunch of programmatic Views. As it appeared to be they all by default have the same id=-1. In order to work with them I need to generate unique id's.
I have tried several approaches - random number generation and based on current time, but anyway there's no 100% guarantee that different Views will have different id's
Just wondering is there any more reliable way to generate unique ones? Probably there's special method/class?
Just want to add to Kaj's answer, from API level 17, you can call
View.generateViewId()
then use the View.setId(int) method.
In case you need it for targets lower than level 17, here is its internal implementation in View.java you can use directly in your project:
private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
/**
* Generate a value suitable for use in {#link #setId(int)}.
* This value will not collide with ID values generated at build time by aapt for R.id.
*
* #return a generated ID value
*/
public static int generateViewId() {
for (;;) {
final int result = sNextGeneratedId.get();
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
int newValue = result + 1;
if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
if (sNextGeneratedId.compareAndSet(result, newValue)) {
return result;
}
}
}
ID number larger than 0x00FFFFFF is reserved for static views defined in the /res xml files. (Most likely 0x7f****** from the R.java in my projects.)
From the code, somehow Android doesn't want you to use 0 as a view's id, and it needs to be flipped before 0x01000000 to avoid the conflits with static resource IDs.
Just an addition to the answer of #phantomlimb,
while View.generateViewId() require API Level >= 17,
this tool is compatibe with all API.
according to current API Level,
it decide weather using system API or not.
so you can use ViewIdGenerator.generateViewId() and View.generateViewId() in the
same time and don't worry about getting same id
import java.util.concurrent.atomic.AtomicInteger;
import android.annotation.SuppressLint;
import android.os.Build;
import android.view.View;
/**
* {#link View#generateViewId()}要求API Level >= 17,而本工具类可兼容所有API Level
* <p>
* 自动判断当前API Level,并优先调用{#link View#generateViewId()},即使本工具类与{#link View#generateViewId()}
* 混用,也能保证生成的Id唯一
* <p>
* =============
* <p>
* while {#link View#generateViewId()} require API Level >= 17, this tool is compatibe with all API.
* <p>
* according to current API Level, it decide weather using system API or not.<br>
* so you can use {#link ViewIdGenerator#generateViewId()} and {#link View#generateViewId()} in the
* same time and don't worry about getting same id
*
* #author fantouchx#gmail.com
*/
public class ViewIdGenerator {
private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
#SuppressLint("NewApi")
public static int generateViewId() {
if (Build.VERSION.SDK_INT < 17) {
for (;;) {
final int result = sNextGeneratedId.get();
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
int newValue = result + 1;
if (newValue > 0x00FFFFFF)
newValue = 1; // Roll over to 1, not 0.
if (sNextGeneratedId.compareAndSet(result, newValue)) {
return result;
}
}
} else {
return View.generateViewId();
}
}
}
Since support library 27.1.0 there's generateViewId() in ViewCompat
ViewCompat.generateViewId()
Create a singleton class, that has an atomic Integer. Bump the integer, and return the value when you need a view id.
The id will be unique during the execution of your process, but wil reset when your process is restarted.
public class ViewId {
private static ViewId INSTANCE = new ViewId();
private AtomicInteger seq;
private ViewId() {
seq = new AtomicInteger(0);
}
public int getUniqueId() {
return seq.incrementAndGet();
}
public static ViewId getInstance() {
return INSTANCE;
}
}
Note that the id might not be unique, if there already are views that have ids in the view 'graph'. You could try to start with a number that is Integer.MAX_VALUE, and decrease it instead of going from 1 -> MAX_VALUE
Regarding the fallback solution for API<17, I see that suggested solutions start generating IDs starting from 0 or 1. The View class has another instance of generator, and also starts counting from number one, which will result in both your and View's generator generating the same IDs, and you will end up having different Views with same IDs in your View hierarchy. Unfortunately there is no a good solution for this but it's a hack that should be well documented:
public class AndroidUtils {
/**
* Unique view id generator, like the one used in {#link View} class for view id generation.
* Since we can't access the generator within the {#link View} class before API 17, we create
* the same generator here. This creates a problem of two generator instances not knowing about
* each other, and we need to take care that one does not generate the id already generated by other one.
*
* We know that all integers higher than 16 777 215 are reserved for aapt-generated identifiers
* (source: {#link View#generateViewId()}, so we make sure to never generate a value that big.
* We also know that generator within the {#link View} class starts at 1.
* We set our generator to start counting at 15 000 000. This gives us enough space
* (15 000 000 - 16 777 215), while making sure that generated IDs are unique, unless View generates
* more than 15M IDs, which should never happen.
*/
private static final AtomicInteger viewIdGenerator = new AtomicInteger(15000000);
/**
* Generate a value suitable for use in {#link View#setId(int)}.
* This value will not collide with ID values generated at build time by aapt for R.id.
*
* #return a generated ID value
*/
public static int generateViewId() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
return generateUniqueViewId();
} else {
return View.generateViewId();
}
}
private static int generateUniqueViewId() {
while (true) {
final int result = viewIdGenerator.get();
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
int newValue = result + 1;
if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
if (viewIdGenerator.compareAndSet(result, newValue)) {
return result;
}
}
}
}

Categories

Resources