How to read-write character devices (like /dev/ttyS0) in Android - android

I have little knowledge of Java and Android. What I am trying to do is to open /dev/ttyS0 in an Android App which should talk to the serial line, but I am getting lost.
My device is rooted, and from a command line I can "echo ...>/dev/ttyS0" and also read from it, but I get lost trying to do that in Java. For start, I can not find a method to open a file in simple read-write mode, without coping with buffers and other intricacies (clearly, I want unbuffered I/O).
I searched the Internet, but all examples refer to USB which is not available for me. Then I've found the UartDevice class, but it is a class to derive a proper implementation from...
I tried to use the File class, and attach to it both a Reader and a Writer class, but the compiler complains and, frankly, I am not sure it is the way to go. I would need a skeleton code to start from; I miss a simple TextFile class with unbuffered read() and write() methods to be used at the same time on the same open file!
Can someone point me in the right direction thanks?

After many tries, and with the help of much information from the SO site, I finally succeded in the task. Here is the code:
public class MainActivity
extends AppCompatActivity {
File serport;
private FileInputStream mSerR;
private FileOutputStream mSerW;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// let this program to access the serial port, and
// turn off the local echo. sudo() is a routine found here on S.O.
sudo("chmod a+rw /dev/ttyS0");
sudo("stty -echo </dev/ttyS0");
// open the file for read and write
serport = new File("/dev/ttyS0");
try {
mSerR = new FileInputStream(serport);
mSerW = new FileOutputStream(serport);
} catch (FileNotFoundException e) {}
// edLine is a textbox where to write a string and send to the port
final EditText edLine = (EditText) findViewById(R.id.edLine);
// edTerm is a multiline text box to show the dialog
final TextView edTerm = findViewById(R.id.edTerm);
// pressing Enter, the content of edLine is echoed and sent to the port
edLine.setOnKeyListener(new View.OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
// If the event is a key-down event on the "enter" button
if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
// Perform action on key press
String cmd = edLine.getText()+"\n";
edTerm.append(cmd);
byte[] obuf = cmd.getBytes();
try {
mSerW.write(obuf);
} catch (IOException e) {}
edLine.setText("");
// read the reply; some time must be granted to the server
// for replying
cmd = "";
int b=-1, tries=8;
while (tries>0) {
try {
b = mSerR.read();
} catch (IOException e) {}
if (b==-1) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {}
--tries;
} else {
tries=3; // allow more timeout (more brief)
if (b==10) break;
cmd = cmd + (char) b;
}
}
// append the received reply to the multiline control
edTerm.append(cmd+"\n");
return true;
}
return false;
}
});
}
}
Please note the presence of the sudo() command in the code: it is there to give r/w permissions to the ttyS0 file, and to disable its echo option. If those permissions+options are already right, or another mean to set them exists, then the sudo() command is not needed.
Note: I believe that the sudo() command implies that the device must be rooted.

All File access in Java is done via input and output streams. If you want to open a file, you simply create a FileOutputStream or FileInputStream for it. These are unbuffered streams. If you then want to write raw bytes you can wrap that in a ByteArrayOutputStream or ByteArrayInputStream.
To do character mode, you can use a Writer. An OutputStreamWriter with a charset of ascii can wrap the FileOutputStream. That should do the character conversion for you. Just don't use a FileWriter- while it seems like the right fit, it has no option to select a character set, and the default is not ascii. For reading in, use an InputStreamReader.

Related

SharedPreferences in Android not persisted to disk when key contains newline

In Android, I'd like to write SharedPreferences key-value pairs where the keys are Base64 strings.
// get a SharedPreferences instance
SharedPreferences prefs = getSharedPreferences("some-name", Context.MODE_PRIVATE);
// generate the base64 key
String someKey = new String(Base64.encode("some-key".getBytes("UTF-8"), Base64.URL_SAFE), "UTF-8");
// write the value for the generated key
prefs.edit().putBoolean(someKey, true).commit();
In the last line, the call to commit returns true. So this key-value pair should have been saved successfully.
When I close and destroy the Activity where this piece of code was used and then re-create the Activity (running this code again), the specified value is returned for the key that we used.
But it turns out that, when I destroy the whole application/process (e.g. using "Force stop" in app settings), the value for our key is lost on the next launch of the Activity.
When I don't use Base64.URL_SAFE but Base64.URL_SAFE | Base64.NO_WRAP as the flags for the Base64 encoding, it works fine.
So this problem has been caused by the newlines at the end of the Base64 keys. Keys like abc can be written without any problems. But when the key is abc\n, it fails.
The problem is that it appears to work without problems first, returning true on commit() and returning the correct preference value on subsequent calls. But when the whole application is destroyed and re-started, the value has not been persisted.
Is this intended behaviour? A bug? Does the documentation say anything about valid key names?
I took a look at GrepCode and see that the operations will be the following (I do not mention useless ones) :
android.app.SharedPreferencesImpl.commit()
android.app.SharedPreferencesImpl.commitToMemory()
android.app.SharedPreferencesImpl.queueDiskWrite(MemoryCommitResult,Runnable)
3.1. XmlUtils.writeMapXml(Map, OutputStream)
3.2. XmlUtils.writeMapXml(Map, String, XmlSerializer)
3.3. XmlUtils.writeValueXml(Object v, String name, XmlSerializer ser)
First : how your data are converted ?
The method XmlUtils.writeValueXml writes the Object value in a XML tag with the attribute name set to the String value. This String value contains exactly the value you specified at the SharedPreference's name.
(And I confirmed this by doing a step-by-step debug with your piece of code).
The XML will be with an unescaped line break character. Actually, the XmlSerializer instance is a FastXmlSerializer instance and it does not escape the \n character (see the link for this class at the end if you want to read the source code)
Interesting piece of code :
writeValueXml(Object v, String name, XmlSerializer out) {
// -- "useless" code skipped
out.startTag(null, typeStr);
if (name != null) {
out.attribute(null, "name", name);
}
out.attribute(null, "value", v.toString());
out.endTag(null, typeStr);
// -- "useless" code skipped
}
Second : why the result is true ?
The commit method has the following code :
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
So it returns the mcr.writeToDiskResult which is set in the SharedPreferencesImpl.writeToFile(MemoryCommitResult) method. Interesting piece of code :
writeToFile(MemoryCommitResult mcr) {
// -- "useless" code skipped
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Libcore.os.stat(mFile.getPath());
synchronized (this) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
mcr.setDiskWriteResult(true);
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// -- "useless" code skipped
}
As we see at the previous point : the XML writing is "ok" (do not throw anything, do not fails), so the sync in the file will be too (just a copy of a Stream in another one, nothing checks the XML content here !).
Currently : your key was converted to (badly formatted) XML and correctly wrote in a File. The result of the whole operation is true as everything went OK. Your changes are comitted to the disk and in the memory.
Third and last : why do I get back the correct value the first time and a bad one the second time
Take a quick look to what happen when we commit the changes to memory in SharedPreferences.Editor.commitToMemory(...) method (interesting part only... :)):
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this) { // magic value for a removal mutation
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
boolean isSame = false;
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
mcr.changesMade = true;
if (hasListeners) {
mcr.keysModified.add(k);
}
}
Important point : the changes are commited to the mMap attribute.
Then, take a quick look to how we get back a value :
public boolean getBoolean(String key, boolean defValue) {
synchronized (this) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
We are taking back the key from mMap (no reading of the value in the file for now). So we have the correct value for this time :)
When you reload your application, you will load the data back from the disk, and so the SharedPreferencesImpl constructor will be called, and it will call the SharedPreferencesImpl.loadFromDiskLocked() method. This method will read the file content and load it in the mMap attribute (I let you see the code by yourself, link provided at the end).
A step-by-step debug shown me that the abc\n was written as abc (with a whitespace character). So, when you will try to get it back, you will never succeed.
To finish, thank you to #CommonsWare to give me a hint about the file content in the comment :)
Links
XmlUtils
FastXmlSerializer
SharedPreferencesImpl
SharedPreferencesImpl.EditorImpl.commit()
SharedPreferencesImpl.EditorImpl.commitToMemory()
SharedPreferencesImpl.enqueueDiskWrite(MemoryCommitResult, Runnable)
SharedPreferencesImpl.writeToFile(MemoryCommitResult)
SharedPreferencesImpl.loadFromDiskLocked()

Getting ProtoBuf.ProtoException using OsmSharp in Xamarin Android

I am trying to test a sample project called Android.Routing.Offline from OsmSharp.Samples in Github.
After two taps on the screen (the first one gets just the GeoCoordinate) I get a ProtoBuf.ProtoException in the Router.cs
private static IBasicRouterDataSource<CHEdgeData> _graph;
public static void Initialize()
{
var routingSerializer = new CHEdgeDataDataSourceSerializer();
_graph = routingSerializer.Deserialize(
Assembly.GetExecutingAssembly().GetManifestResourceStream(#"Android.Routing.Offline.kempen-big.contracted.mobile.routing"));
}
public static Route Calculate(GeoCoordinate from, GeoCoordinate to)
{
try
{
lock(_graph)
{
var router = Router.CreateCHFrom(_graph, new CHRouter(), new OsmRoutingInterpreter());
// The exception happens here below
var fromResolved = router.Resolve(Vehicle.Car, from);
var toResolved = router.Resolve(Vehicle.Car, to);
if(fromResolved != null && toResolved !=null)
{
return router.Calculate(Vehicle.Car, fromResolved, toResolved);
}
}
}
catch(Exception ex)
{
OsmSharp.Logging.Log.TraceEvent("Router", OsmSharp.Logging.TraceEventType.Critical, "Unhandled exception occured: {0}", ex.ToString());
}
return null;
}
And the exception:
> {ProtoBuf.ProtoException: Invalid wire-type; this usually means you
> have over-written a file without truncating or setting the length; see
> http://stackoverflow.com/q/2152978/23354 at
> ProtoBuf.ProtoReader.ReadSingle () ...
I didnt overwrite the file (kempen-big.contracted.mobile.routing) just added it as a linked file in the project. Any ideas how I can solve this issue?
Well, the first thing to try is to check that the contents of the Stream you are reading (via GetManifestResourceStream) contains exactly the contents you are expecting, and not some wrapper or otherwise-corrupt mess. If you have some checksum algorithm you can run: great! Checking just the .Length would be a great start. Otherwise, you could cheat (just for the purposes of validating the contents) by getting the hex:
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
string hex = BitConverter.ToString(
ms.GetBuffer(), 0, (int)ms.Length);
// dump this string, and compare it to the same output run on the
// oringal file; they should be identical
}
Note that this duplicates the contents in-memory, purely so we can get a byte[] (oversized) to get the hex from - it isn't intended for "real" code, but until you are sure that the contents are correct, all other bets are off. I strongly suspect that you'll find that the contents are not identical to the contents in the original file. Note that I'm also implicitly assuming that the original file works fine in terms of deserialization. If the original file doesn't work: again, all bets are off.

Building a Terminal Emulator for Android

I've been trying to build a Terminal Emulator for Android. Being pretty new to this, my idea was to execute each command and store the output in a file, whose contents would be displayed after each execution.
Pseudo Code :
public Boolean execCommands(String command) {
try {
rt = Runtime.getRuntime();
process = rt.exec("su");
DataOutputStream os = new DataOutputStream(process.getOutputStream());
os.writeBytes("echo $ \""+command+ "\" >> /sdcard/Android/data/terminalemulatorlog.txt\n\n\n");
/**** Note : String command = (EditText)findViewById(R.id.command).getText().toString(); ****/
os.flush();
os.writeBytes("exit\n");
os.flush();
process.waitFor();
}
// Error Handling
displayOutput(); //Loads and displays the Text File (/sdcard/Android/data/terminalemulatorlog.txt)
return true;
}
This piece of code works except for a few special commands (Eg. 'clear').
But what I'm more concerned about are the following problems :
Each time a command is to be executed, I end up seeking SuperUser permissions (second line of code). And I'd like to do away with this.
In cases when the user enters one command followed by another,
Such as :
cd /sdcard
touch File.txt
The File.txt is created in '/' and not in '/sdcard'. As of now to avoid this, I'm keeping a track of all the 'cd' commands to figure out what the present working directory is. And I'm hoping that there is a better way around this.
I'd be grateful if someone could help me out here.
Not sure if you are still needing this or not, but here is how I am issuing multiple commands at one time and not using "su" to have them run.
try {
String[] commands = {
"dumpstate > /sdcard/LogFiles/dumpstate.txt",
"dumpsys > /sdcard/LogFiles/dumpsys.txt",
"logcat -d > /sdcard/LogFiles/log.txt",
"cat /sdcard/LogFiles/dumpstate.txt /sdcard/LogFiles/dumpsys.txt /sdcard/LogFiles/log.txt > /sdcard/LogFiles/bugreport.rtf" };
Process p = Runtime.getRuntime().exec("/system/bin/sh -");
DataOutputStream os = new DataOutputStream(p.getOutputStream());
for (String tmpCmd : commands) {
os.writeBytes(tmpCmd + "\n");
}
} catch (IOException e) {
e.printStackTrace();
}
This a bit late but here a few ways of doing this.
1)
Instead of using su as a starting point use /system/bin/sh.
and after calling
rt.exec("/system/bin/sh");
You should hold onto the Output Stream and Input Stream to give further commands.
After you issued a command you should echo a magic line like "---EOF---" and stop reading input after reading that line. If you don't have this you'll end up with the read function from the InputStream blocking.
2) Pipe the data to a native process you've written that simply moves the data on to your Android Application with a terminating character or string attached to the end.
I am not entirely sure how to do this, but it is essentially the same as the previous method just relies on you native application as a middle man.
This will get you close to a functioning "Terminal Emulator".
3)If you wan't a true Ternimal Emulator then there's no other way to do it than : using a native application that opens a connection to a psuedoterminal.
Here's some basic information of how to open a pty : link
Terminal Emulator is a open source project that uses this technique.
Have a look here
Regarding problem 1:
Each time a command is to be executed, I end up seeking SuperUser permissions (second line of code). And I'd like to do away with this.
Thanks to Xonar's suggestion from another answer:
After you issued a command you should echo a magic line like "---EOF---" and stop reading input after reading that line.
Solution in Kotlin:
private lateinit var suProcess: Process
private lateinit var outputStream: DataOutputStream
private fun getSu(): Boolean {
return try {
suProcess = Runtime.getRuntime().exec("su")
outputStream = DataOutputStream(suProcess.outputStream)
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
private fun sudo(command: String): List<String>? {
return try {
outputStream.writeBytes("$command\n")
outputStream.flush()
outputStream.writeBytes("echo ---EOF---\n")
outputStream.flush()
val reader = suProcess.inputStream.bufferedReader()
val result = mutableListOf<String>()
while (true) {
val line = reader.readLine()
if (line == "---EOF---") break
result += line
}
result
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private fun exitTerminal() {
try {
outputStream.writeBytes("exit\n")
outputStream.flush()
suProcess.waitFor()
} catch (e: Exception) {
e.printStackTrace()
} finally {
outputStream.close()
}
}
//Activity method
override fun onDestroy() {
super.onDestroy()
exitTerminal()
}

Creating and Storing Log File on device in Android

I am planning to automate the testing of an application by creating a log to store some results of execution of the app and latter on parse it using a piece of python code and plot a graph.
The application is a WiFi fingerprinter i.e it collects info such as mac id, rss(recieved signal strength and rank(normalized rss) about the wifi devices in the surrounding environment. So to test this application I would have to take it to the location and record the results(as of now manually). So logcat wouldn't serve the purpose.
Automation requires
1. Storing the log in the device
2. Access to the log file in the system through usb
Format of the Log file:
Snapshot: 1
Fingerprint: 1, Rank: 0.23424, Boolean: true
Fingerprint: 2, Rank: 0.42344, Boolean: false
Fingerprint: 3, Rank: 0.23425, Boolean: true
Snapshot: 2
Fingerprint: 1, Rank: 0.75654, Boolean: false
Fingerprint: 2, Rank: 0.23456, Boolean: true
Fingerprint: 3, Rank: 0.89423, Boolean: true
................
Now I know there are basically 3 approaches for persistent storage(SharedPrefs wouldn't suit this scenario anyway). I tried Internal Storage, but even after setting the mode of the file as MODE_WORLD_READABLE it was impossible to read the file using Device File Explorer in Eclipse.
I am still wary of using external storage for storing the log. Any tutorial on how to write to a file in usb of the device will definitely help.
I thought of structuring the data to be stored so as to use SQLite for storage. But this establishing many unnecessary relations(foreign and domestic) between data and make it complex. If there is no way around, then here be dragons.
Basically I want to write to a file(easier I suppose) in the device and latter on read it in my system by connecting to it via usb. Any help on how to do it would be much appreciated.
Wary or not, External Storage still may be the only way to go. Without root access on the device, you can't really get at anything "Internal" unless you're going to be okay with reading within an application on the device. The docs provide pretty solid guidelines for where to create external files, and if you are using API Level 8 or higher, there are a couple of extra functions that can be used. I'm sure you know this page, but here it is anyway: http://developer.android.com/guide/topics/data/data-storage.html#filesExternal
If you're in need of any file io example code... I think I could dig some up...
EDIT - I would start by following the guidelines in the above docs to first confirm the state of the storage. I unfortunately don't have any experience with appending a file in Java, so someone else would definitely be more qualified to answer. This doesn't cover appending, but I have a backup routine in one of my personal apps that looks something like this.
File backupPath = Environment.getExternalStorageDirectory();
backupPath = new File(backupPath.getPath() + "/Android/data/com.maximusdev.bankrecord/files");
if(!backupPath.exists()){
backupPath.mkdirs();
}
FileOutputStream fos;
try {
fos = new FileOutputStream(backupPath.getPath() + "/recordsbackup.txt");
if(okaytowrite){
for(int i = 0; i < count; ++i){
entry = adapter.getItem(i);
fos.write(entry.toString().getBytes());
fos.write("\n".getBytes());
fos.write(String.valueOf(entry.dateTime).getBytes());
fos.write("\n".getBytes());
fos.write(String.valueOf(entry.sign).getBytes());
fos.write("\n".getBytes());
fos.write(String.valueOf(entry.cleared).getBytes());
fos.write("\n".getBytes());
fos.write(String.valueOf(entry.transDate).getBytes());
fos.write("\n".getBytes());
fos.write(entry.category.getBytes());
fos.write("\n".getBytes());
}
}
fos.close();
Toast.makeText(this, "Backup Complete", Toast.LENGTH_SHORT).show();
} catch (FileNotFoundException e) {
e.printStackTrace();
AlertDialog.Builder delmessagebuilder = new AlertDialog.Builder(this);
delmessagebuilder.setCancelable(false);
delmessagebuilder.setMessage("File Access Error");
delmessagebuilder.setNeutralButton("Okay", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
}
});
delmessagebuilder.create().show();
} catch (IOException e) {
e.printStackTrace();
AlertDialog.Builder delmessagebuilder = new AlertDialog.Builder(this);
delmessagebuilder.setCancelable(false);
delmessagebuilder.setMessage("File Access Error");
delmessagebuilder.setNeutralButton("Okay", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
}
});
delmessagebuilder.create().show();
}
Once I'm ready to write, I'm pulling a custom object (entry) out of an ArrayAdapter (adapter) and converting field valuse to strings and using getBytes() to pass to the FileOutputStream write function. I've done some research and there are quite a few other options for file writing in Java/Android... the FileWriter Class for instance, so it bears further research.
I used a very simple approach to write String messages to the log file by creating a FileWriter object.
public static BufferedWriter out;
private void createFileOnDevice(Boolean append) throws IOException {
/*
* Function to initially create the log file and it also writes the time of creation to file.
*/
File Root = Environment.getExternalStorageDirectory();
if(Root.canWrite()){
File LogFile = new File(Root, "Log.txt");
FileWriter LogWriter = new FileWriter(LogFile, append);
out = new BufferedWriter(LogWriter);
Date date = new Date();
out.write("Logged at" + String.valueOf(date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds() + "\n"));
out.close();
}
}
Now the function to write a new message to the log file.
public void writeToFile(String message){
try {
out.write(message+"\n");
out.close();
} catch (IOException e) {
e.printStackTrace();
}

Is there a simpler way to do parsing in Java on Android?

I'm having difficulty performing what should be a simple task. Let's say I have a text file that states:
a = b
I want my program to read this file and output "b" whenever the user inputs "a". In Python or C++ I can accomplish this in around 7 lines or less.
However, I'm having difficulty finding a simple way to do this on Android. For example, one sample that I found here on SO had nearly 900 lines in 6 files.
Is there a simple way to parse a file and return a variable on Android that I am missing?
BufferedReader r = new BufferedReader(new FileReader(filename));
string s;
while(s = r.readLine()) {
//Oh hey, I got a line out of the file.
//In three lines of code.
//So what's all this Android-bashing about?
}
As long as you're happy with thea = b format used in properties files, you get 99% of the way to your goal with
Properties properties = new Properties();
try {
properties.load(new FileInputStream(filename));
} catch (IOException e) {
// the file is missing or is not in 'a = b' format
}
The, having got a variable key containing the string "a" from the user, the result of properties.getProperty ( key ) will equal "b" if the file contains the line a = b. I'm pretty sure you need more than that in C++ to load a map from a file and handle all the escaping and character encoding issues.
If the properties are held in a text file called mappings.properties in the assets folder of your Android project rather than in the user's file system, then you get at it like this:
final AssetManager am = getResources().getAssets();
final Properties properties = new Properties();
try {
properties.load( am.open("mappings.properties"));
} catch (IOException e) {
// the file is missing or is not in 'a = b' format
}
This next bit is borrowed from the android tutorial to show a toast message with 'b' in it if 'a' is entered in the edit box. Maybe here is where you get your line count from, as setting up a GUI with XML files and adding listeners in Java is fairly verbose compared with other languages. That's due to Java and XML syntax rather than the virtual machine.
final EditText edittext = (EditText) findViewById(R.id.edittext);
edittext.setOnKeyListener(new OnKeyListener() {
#Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
// If the event is a key-down event on the "enter" button
if ((event.getAction() == KeyEvent.ACTION_DOWN) &&
(keyCode == KeyEvent.KEYCODE_ENTER)) {
// Perform action on key press
Toast.makeText(YourOuterViewClass.this,
properties.getProperty(edittext.getText().toString()),
Toast.LENGTH_SHORT).show();
return true;
}
return false;
}
});
You definitely don't need hundreds of lines of code to achieve this. It can be done in a few lines. I don't know what examples you were looking at but they were probably doing way more than what you are describing.

Categories

Resources