In dual-SIM card mobiles I manage to differentiate SIM cards in the calllog using the PHONE_ACCOUNT_ID property as shown in the code below. Now I need to know what SIM card actually was use (1 or 2) to make or receive the call. PHONE_ACCOUNT_ID shows something like this 8953011201104578086F for and one SIM card and similar, but no equal to the other. This was tested in a Samsung mobile:
fun readCallLog() {
val cursor = context.contentResolver.query(CallLog.Calls.CONTENT_URI,null, null, null, CallLog.Calls.DATE + " DESC")
val number = cursor?.getColumnIndex(CallLog.Calls.NUMBER)
val date = cursor?.getColumnIndex(CallLog.Calls.DATE)
val type = cursor?.getColumnIndex(CallLog.Calls.TYPE)
val account_id = cursor?.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ID)
val tmp : MutableList<List<String?>> = mutableListOf()
while (cursor?.moveToNext() == true ) {
val call_number = if (number != null) cursor.getString(number) else ""
val call_date = if(date != null) cursor.getString(date) else ""
val call_type = if(type != null) cursor.getInt(type).toString() else ""
val call_account_id = if(account_id != null) cursor.getString(account_id) else ""
tmp.add( listOf(call_number, call_date, call_type, call_account_id))
}
}
You can get information on SIM cards with SubscriptionManager.getActiveSubscriptionInfoList().
On some devices, Call.PHONE_ACCOUNT_ID equals subscriptionInfo.getSubscriptionId(), however on other devices (your case) subscriptionInfo.getIccId() is a substring of it, so you need to check both.
See also SubscriptionManager reference.
The official way is to check the account-id (documentation here), but on some devices it just returns the SIM card slot index, so here's the code with a workaround (from Android 6.0 (Marshmallow)):
fun getSimSlotIndexFromAccountId(context: Context, accountIdToFind: String): Int {
// This is actually the official data that should be found, as on the emulator, but sadly not all phones return here a proper value
val telecomManager = context.getSystemService<TelecomManager>()
telecomManager.callCapablePhoneAccounts.forEachIndexed { index, account: PhoneAccountHandle ->
val phoneAccount: PhoneAccount = telecomManager.getPhoneAccount(account)
val accountId: String = phoneAccount.accountHandle
.id
if (accountIdToFind == accountId) {
return index
}
}
accountIdToFind.toIntOrNull()?.let {
if (it >= 0)
return it
}
return -1
}
Usage:
val simIdColumnIndex = callLogCursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ID)
val accountId: String = callLogCursor.getString(simIdColumnIndex)
val simCardSlotIndex = getSimSlotIndexFromAccountId(applicationContext, accountId)
I've reported about this issue (that some devices don't follow official API) here:
Bug: on some devices, PHONE_ACCOUNT_ID just returns the SIM-card slot index
Related
I have to get the Advertisement Interval in milliseconds. I used result.periodicAdvertisingInterval but this returns 0. I have to implement something like this:
private val scanCallback = object : ScanCallback(){
#RequiresApi(Build.VERSION_CODES.N)
#SuppressLint("MissingPermission", "NotifyDataSetChanged")
override fun onScanResult(callbackType: Int, result: ScanResult) {
val scanJob = CoroutineScope(Dispatchers.Main).launch {
val tag = deviceMap.computeIfAbsent(result.device.address) {
val newTag = BleTag(result.device.name ?: "Unbekannt", result.device.address, result.rssi , result.scanRecord?.bytes, "")
deviceList.add(newTag)
newTag
}
tag.name = result.device.name ?: "Unbekannt"
tag.rssi = result.rssi
tag.advertisementData = result.scanRecord?.bytes
}
deviceList.sortBy {result.rssi }
recyclerView.adapter?.notifyDataSetChanged()
menu.findItem(R.id.count).title = "Geräte: " + deviceList.size
super.onScanResult(callbackType, result)
}
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
Log.e("Scan failed","")
}
}
This result is obtained by subtracting the timestamps of two consecutive advertisements of the same device.
The question says it all, i have a function to which i pass a phone number and the function asigns a predownloaded ringtone to a contact with the passed phone number (if such a contact exists). The problem is that it only works on Android 9 and below, when i run it on an Android 10 it just does nothing, no exceptions, no crashes, nothing..
Heres the functions if anybody knows whats up:
private fun setCustomRingtone(targetContactPhone: String): String {
val lookupUri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, targetContactPhone)
val projection = arrayOf<String>(Contacts._ID, Contacts.LOOKUP_KEY)
val data = contentResolver.query(lookupUri, projection, null, null,null)!!
data.moveToFirst()
try {
// Get the contact lookup Uri
val contactID: Long = data.getLong(0)
val lookupKey = data.getString(1);
val contactUri = Contacts.getLookupUri(contactID, lookupKey)
if (contactUri == null){
Log.i(TAG_LABEL, "contactUri je null, invalid arguments?")
return "fail";
}
Log.i(TAG_LABEL, "contact uri: " + contactUri.toString()) // valid and expected Uri
// Get the path of the ringtone you'd like to use
val storage = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
Log.i(TAG_LABEL, "out: $storage")
val file = File(storage, "ringtone.mp3")
Log.i(TAG_LABEL, file.exists().toString()) // reasures me that the file exists
val value = Uri.fromFile(file).toString()
Log.i(TAG_LABEL, "size: " + file.length().toString() + " name: " + file.name)
val values = ContentValues()
values.put(Contacts.CUSTOM_RINGTONE, value)
// TRIED A COMBINATION OF THESE, THEY DON'T WORK
// values.put(MediaStore.MediaColumns.TITLE, file.nameWithoutExtension);
// values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/mpeg");
// values.put(MediaStore.MediaColumns.SIZE, file.length())
// values.put(MediaStore.Audio.Media.IS_RINGTONE, true)
// values.put(MediaStore.Audio.Media.IS_NOTIFICATION, false);
// values.put(MediaStore.Audio.Media.IS_ALARM, false);
// values.put(MediaStore.Audio.Media.IS_MUSIC, false);
// values.put(MediaStore.Audio.Media.ARTIST, "example")
val res = contentResolver.update(contactUri, values, null, null)
Log.i(TAG_LABEL, "content resolver res: " + res.toString())
}finally {
data.close();
}
return "succes"
}
Once again, it runs fine on Android 9 and below. Also, i can't seem to find any information in the official documentation that mentions changes in Contacts services or similar.
I cannot understand exactly where I made a stupid mistake. I would be very grateful for the advice
val f:File = File("drawable/paystack_mark.png")
// val path:Path = Paths.get("drawable/new_logo.jpg")
val attr = Files.readAttributes<BasicFileAttributes>(file.toPath(), BasicFileAttributes::class.java)
or
val path:Path = Paths.get("drawable/new_logo.jpg")
val attr = Files.readAttributes<BasicFileAttributes>(path, BasicFileAttributes::class.java)
I cannot understand exactly where I made a stupid mistake. I would be very grateful for the advice
Your second example is working fine, I have tried it on windows with a valid path to a .png file.
This is how I used it (just printed the attr.creationTime()):
fun main(args: Array<String>) {
val path: Path = Paths.get("M:\\y\\path\\to\\the\\image.png")
val attr = Files.readAttributes<BasicFileAttributes>(path, BasicFileAttributes::class.java)
println("creation time: " + attr.creationTime())
}
The output was just
creation time: 2018-03-16T13:11:57.40283Z
Keep in mind that you are coding for Android, so maybe the String-versions of the paths are different (see the backslashes for the path on a windows machine).
This is work for me and thank you
// read image from gallary or you can give direct path
getImage()// read image
val contentURI = data?.getData()
var imageFile = File(contentURI?.let { getRealPathFromURI(it) })
val lastModifiedDate: Date = Date(imageFile.lastModified())
println("creation time: " + attr.creationTime())
// get Native URI Function
private fun getRealPathFromURI(contentURI: Uri): String {
var getApplicationContext = getContext()?.getContentResolver();
var result: String
var cursor: Cursor? =
context?.getContentResolver()?.query(contentURI, null, null, null, null);
if (cursor == null) { // Source is Dropbox or other similar local file path
result = contentURI.getPath().toString();
} else {
cursor.moveToFirst();
val idx: Int = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
result = cursor.getString(idx);
cursor.close();
}
return result;
}
On this line in
val contactUri: Uri? = data.data
…
val cursor = requireActivity().contentResolver
.query(**contactUri**, queryFields, null, null, null)
Type mismatch required Uri got Uri
How do you fix this?
Code sample
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when {
resultCode != Activity.RESULT_OK -> return
requestCode == REQUEST_CONTACT && data != null -> {
val contactUri: Uri? = data.data
// Specify which fields query to return values for.
val queryFields = arrayOf(ContactsContract.Contacts.DISPLAY_NAME)
// Perform query
val cursor = requireActivity().contentResolver
.query(contactUri, queryFields, null, null, null)
cursor?.use {
// Double-check that you actually got results
if (it.count == 0) {
return
}
// Pull out the first column of the first row of data
it.moveToFirst()
val suspect = it.getString(0)
crime.suspect = suspect
crimeDetailViewModel.saveCrime(crime)
suspectButton.text = suspect
}
}
}
}
Either try removing the ? after your uri so:
val contactUri: Uri = data.data
instead of
val contactUri: Uri? = data.data
Or if that doesn't work, check this answer out
Background
We allow the user to create some text that will get converted to HTML, using a rich-text editor library (called Android-RTEditor).
The output HTML text is saved as is on the server and the device.
Because on some end cases, there is a need to show a lot of this content (multiple instances), we wish to also save a "preview" version of this content, meaning it will be much shorter in length (say 120 of normal characters, excluding the extra characters for the HTML tags, which are not counted).
What we want is a minimized version of the HTML. Some tags might optionally be removed, but we still want to see lists (numbered/bullets), no matter what we choose to do, because lists do show like text to the user (the bullet is a character, and so do the numbers with the dot).
The tag of going to next line should also be handled , as it's important to go to the next line.
The problem
As opposed to a normal string, where I can just call substring with the required number of characters, on HTML it might ruin the tags.
What I've tried
I've thought of 2 possible solutions for this:
Convert to plain text (while having some tags handled), and then truncate : Parse the HTML, and replacing some tags with Unicode alternatives, while removing the others. For example, instead of a bullet-list, put the bullet character (maybe this), and same for numbered list (put numbers instead). All the other tags would be removed. Same goes for the tag of going to the next line (""), which should be replaced with "\n". After that, I could safely truncate the normal text, because there are no more tags that could be ruined.
Truncate nicely inside the HTML : Parse the HTML, while identifying the text within it, and truncate it there and closing all tags when reaching the truncation position. This might even be harder.
I'm not sure which is easier, but I can think of possible disadvantages for each. It is just a preview though, so I don't think it matters much.
I've searched the Internet for such solutions, to see if others have made it.
I've found some links that talk about "cleaning" or "optimizing" HTML, but I don't see they can handle replacing them or truncating them. Not only that, but since it's HTML, most are not related to Android, and use PHP, C#, Angular and others as their language.
Here are some links that I've found:
Java Library to truncate html strings?
how to truncate HTML string without leaving it malformated?
The questions
Are those solutions that I've written possible? If so, is there maybe a known way to implement them? Or even a Java/Kotlin/Android library? How hard would it be to make such a solution?
Maybe other solution I haven't thought about?
EDIT:
I've also tried using an old code I've made in the past (here), which parses XML. Maybe it will work. I also try now to investigate some third party libraries for parsing HTML, such as Jsoup. I think it can help with the truncating, while supporting "faulty" HTML inputs.
OK, I think I got it, using my old code for converting XML string into an object . It would still be great to see more robust solutions, but I think what I got is good enough, at least for now.
Below code uses it (origininal XmlTag class available here) :
XmlTagTruncationHelper.kt
object XmlTagTruncationHelper {
/**#param maxLines max lines to permit. If <0, means there is no restriction
* #param maxTextCharacters max text characters to permit. If <0, means there is no restriction*/
class Restriction(val maxTextCharacters: Int, val maxLines: Int) {
var currentTextCharactersCount: Int = 0
var currentLinesCount: Int = 0
}
#JvmStatic
fun truncateXmlTag(xmlTag: XmlTag, restriction: Restriction): String {
if (restriction.maxLines == 0 || (restriction.maxTextCharacters >= 0 && restriction.currentTextCharactersCount >= restriction.maxTextCharacters))
return ""
val sb = StringBuilder()
sb.append("<").append(xmlTag.tagName)
val numberOfAttributes = if (xmlTag.tagAttributes != null) xmlTag.tagAttributes!!.size else 0
if (numberOfAttributes != 0)
for ((key, value) in xmlTag.tagAttributes!!)
sb.append(" ").append(key).append("=\"").append(value).append("\"")
val numberOfInnerContent = if (xmlTag.innerTagsAndContent != null) xmlTag.innerTagsAndContent!!.size else 0
if (numberOfInnerContent == 0)
sb.append("/>")
else {
sb.append(">")
for (innerItem in xmlTag.innerTagsAndContent!!) {
if (restriction.maxTextCharacters >= 0 && restriction.currentTextCharactersCount >= restriction.maxTextCharacters)
break
if (innerItem is XmlTag) {
if (restriction.maxLines < 0)
sb.append(truncateXmlTag(innerItem, restriction))
else {
// Log.d("AppLog", "xmlTag:" + innerItem.tagName + " " + innerItem.innerTagsAndContent?.size)
var needToBreak = false
when {
innerItem.tagName == "br" -> {
++restriction.currentLinesCount
needToBreak = restriction.currentLinesCount >= restriction.maxLines
}
innerItem.tagName == "li" -> {
++restriction.currentLinesCount
needToBreak = restriction.currentLinesCount >= restriction.maxLines
}
}
if (needToBreak)
break
sb.append(truncateXmlTag(innerItem, restriction))
}
} else if (innerItem is String) {
if (restriction.maxTextCharacters < 0)
sb.append(innerItem)
else
if (restriction.currentTextCharactersCount < restriction.maxTextCharacters) {
val str = innerItem
val extraCharactersAllowedToAdd = restriction.maxTextCharacters - restriction.currentTextCharactersCount
val strToAdd = str.substring(0, Math.min(str.length, extraCharactersAllowedToAdd))
if (strToAdd.isNotEmpty()) {
sb.append(strToAdd)
restriction.currentTextCharactersCount += strToAdd.length
}
}
}
}
sb.append("</").append(xmlTag.tagName).append(">")
}
return sb.toString()
}
}
XmlTag.kt
//based on https://stackoverflow.com/a/19115036/878126
/**
* an xml tag , includes its name, value and attributes
* #param tagName the name of the xml tag . for example : <a>b</a> . the name of the tag is "a"
*/
class XmlTag(val tagName: String) {
/** a hashmap of all of the tag attributes. example: <a c="d" e="f">b</a> . attributes: {{"c"="d"},{"e"="f"}} */
#JvmField
var tagAttributes: HashMap<String, String>? = null
/**list of inner text and xml tags*/
#JvmField
var innerTagsAndContent: ArrayList<Any>? = null
companion object {
#JvmStatic
fun getXmlFromString(input: String): XmlTag? {
val factory = XmlPullParserFactory.newInstance()
factory.isNamespaceAware = true
val xpp = factory.newPullParser()
xpp.setInput(StringReader(input))
return getXmlRootTagOfXmlPullParser(xpp)
}
#JvmStatic
fun getXmlRootTagOfXmlPullParser(xmlParser: XmlPullParser): XmlTag? {
var currentTag: XmlTag? = null
var rootTag: XmlTag? = null
val tagsStack = Stack<XmlTag>()
xmlParser.next()
var eventType = xmlParser.eventType
var doneParsing = false
while (eventType != XmlPullParser.END_DOCUMENT && !doneParsing) {
when (eventType) {
XmlPullParser.START_DOCUMENT -> {
}
XmlPullParser.START_TAG -> {
val xmlTagName = xmlParser.name
currentTag = XmlTag(xmlTagName)
if (tagsStack.isEmpty())
rootTag = currentTag
tagsStack.push(currentTag)
val numberOfAttributes = xmlParser.attributeCount
if (numberOfAttributes > 0) {
val attributes = HashMap<String, String>(numberOfAttributes)
for (i in 0 until numberOfAttributes) {
val attrName = xmlParser.getAttributeName(i)
val attrValue = xmlParser.getAttributeValue(i)
attributes[attrName] = attrValue
}
currentTag.tagAttributes = attributes
}
}
XmlPullParser.END_TAG -> {
currentTag = tagsStack.pop()
if (!tagsStack.isEmpty()) {
val parentTag = tagsStack.peek()
parentTag.addInnerXmlTag(currentTag)
currentTag = parentTag
} else
doneParsing = true
}
XmlPullParser.TEXT -> {
val innerText = xmlParser.text
if (currentTag != null)
currentTag.addInnerText(innerText)
}
}
eventType = xmlParser.next()
}
return rootTag
}
/**returns the root xml tag of the given xml resourceId , or null if not succeeded . */
fun getXmlRootTagOfXmlFileResourceId(context: Context, xmlFileResourceId: Int): XmlTag? {
val res = context.resources
val xmlParser = res.getXml(xmlFileResourceId)
return getXmlRootTagOfXmlPullParser(xmlParser)
}
}
private fun addInnerXmlTag(tag: XmlTag) {
if (innerTagsAndContent == null)
innerTagsAndContent = ArrayList()
innerTagsAndContent!!.add(tag)
}
private fun addInnerText(str: String) {
if (innerTagsAndContent == null)
innerTagsAndContent = ArrayList()
innerTagsAndContent!!.add(str)
}
/**formats the xmlTag back to its string format,including its inner tags */
override fun toString(): String {
val sb = StringBuilder()
sb.append("<").append(tagName)
val numberOfAttributes = if (tagAttributes != null) tagAttributes!!.size else 0
if (numberOfAttributes != 0)
for ((key, value) in tagAttributes!!)
sb.append(" ").append(key).append("=\"").append(value).append("\"")
val numberOfInnerContent = if (innerTagsAndContent != null) innerTagsAndContent!!.size else 0
if (numberOfInnerContent == 0)
sb.append("/>")
else {
sb.append(">")
for (innerItem in innerTagsAndContent!!)
sb.append(innerItem.toString())
sb.append("</").append(tagName).append(">")
}
return sb.toString()
}
}
Sample usage:
build.grade
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
...
dependencies{
implementation 'com.1gravity:android-rteditor:1.6.7'
...
}
...
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// val inputXmlString = "<zz>Zhshs<br/>ABC</zz>"
val inputXmlString = "Aaa<br/><b>Bbb<br/></b>Ccc<br/><ul><li>Ddd</li><li>eee</li></ul>fff<br/><ol><li>ggg</li><li>hhh</li></ol>"
// XML must have a root tag
val xmlString = if (!inputXmlString.startsWith("<"))
"<html>$inputXmlString</html>" else inputXmlString
val rtApi = RTApi(this, RTProxyImpl(this), RTMediaFactoryImpl(this, true))
val mRTManager = RTManager(rtApi, savedInstanceState)
mRTManager.registerEditor(beforeTruncationTextView, true)
mRTManager.registerEditor(afterTruncationTextView, true)
beforeTruncationTextView.setRichTextEditing(true, inputXmlString)
val xmlTag = XmlTag.getXmlFromString(xmlString)
Log.d("AppLog", "xml parsed: " + xmlTag.toString())
val maxTextCharacters = 10
val maxLines = 20
val output = XmlTagTruncationHelper.truncateXmlTag(xmlTag!!, XmlTagTruncationHelper.Restriction(maxTextCharacters, maxLines))
afterTruncationTextView.setRichTextEditing(true, output)
Log.d("AppLog", "xml with truncation : maxTextCharacters: $maxTextCharacters , maxLines: $maxLines output: " + output)
}
}
activity_main.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"
tools:context=".MainActivity">
<com.onegravity.rteditor.RTEditText
android:id="#+id/beforeTruncationTextView" android:layout_width="match_parent"
android:layout_height="wrap_content" android:background="#11ff0000" tools:text="beforeTruncationTextView"/>
<com.onegravity.rteditor.RTEditText
android:id="#+id/afterTruncationTextView" android:layout_width="match_parent"
android:layout_height="wrap_content" android:background="#1100ff00" tools:text="afterTruncationTextView"/>
</LinearLayout>
And the result: