Issue with Phonegap and MillennialMedia - android

I'm trying to use MillenialMedia on my Phonegap app, but i just can't make it work.
I get the following errors on the LogCat:
08-12 12:52:36.755: I/MillennialMediaSDK(333): Initializing MMLayout.
08-12 12:52:37.385: W/MillennialMediaSDK(333): MMLayout adding view (MMWebView originally from(1) MRaidState(loading).) to AdType[(b) InternalId(1) LinkedId(0) isFinishing(false)]
08-12 12:52:37.645: W/MillennialMediaSDK(333): AdView onLayout changedtrue int left 0 int top 687 int right 480 int bottom 762
08-12 12:52:37.685: W/MillennialMediaSDK(333): Id check for parent: 1 versus 1
08-12 12:52:38.355: I/MillennialMediaSDK(333): Millennial ad return failed. Zero content length returned.
08-12 12:52:38.965: E/MillennialMediaSDK(333): Could not get a handshake. Not trusted server certificate
Here's my code:
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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 com.logicstudio.paradise.Replay;
import android.os.Bundle;
import org.apache.cordova.*;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
public class Replay extends DroidGap
{
//Constants for tablet sized ads (728x90)
private static final int IAB_LEADERBOARD_WIDTH = 728;
private static final int IAB_LEADERBOARD_HEIGHT = 90;
private static final int MED_BANNER_WIDTH = 480;
private static final int MED_BANNER_HEIGHT = 60;
//Constants for phone sized ads (320x50)
private static final int BANNER_AD_WIDTH = 320;
private static final int BANNER_AD_HEIGHT = 50;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.setIntegerProperty("splashscreen", R.drawable.splash);
super.loadUrl(Config.getStartUrl());
int placementWidth = BANNER_AD_WIDTH;
int placementHeight = BANNER_AD_HEIGHT;
//Finds an ad that best fits a users device.
if(canFit(IAB_LEADERBOARD_WIDTH)) {
placementWidth = IAB_LEADERBOARD_WIDTH;
placementHeight = IAB_LEADERBOARD_HEIGHT;
}
else if(canFit(MED_BANNER_WIDTH)) {
placementWidth = MED_BANNER_WIDTH;
placementHeight = MED_BANNER_HEIGHT;
}
com.millennialmedia.android.MMSDK.initialize(this);
com.millennialmedia.android.MMAdView adView = new com.millennialmedia.android.MMAdView(this);
LinearLayout layout = super.root;
adView.setApid("131468");
//Set your metadata in the MMRequest object
com.millennialmedia.android.MMRequest request = new com.millennialmedia.android.MMRequest();
//Add the MMRequest object to your MMAdView.
adView.setMMRequest(request);
//Sets the id to preserve your ad on configuration changes.
adView.setId(com.millennialmedia.android.MMSDK.getDefaultAdId());
//Set the ad size. Replace the width and height values if needed.
adView.setWidth(placementWidth);
adView.setHeight(placementHeight);
//Calculate the size of the adView based on the ad size. Replace the width and height values if needed.
int layoutWidth = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, placementWidth, getResources().getDisplayMetrics());
int layoutHeight = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, placementHeight, getResources().getDisplayMetrics());
//Create the layout parameters using the calculated adView width and height.
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(layoutWidth, layoutHeight);
//This positions the banner.
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
adView.setLayoutParams(layoutParams);
//Add the adView to the layout. The layout is assumed to be a RelativeLayout.
layout.addView(adView);
adView.getAd();
}
protected boolean canFit(int adWidth) {
int adWidthPx = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, adWidth, getResources().getDisplayMetrics());
DisplayMetrics metrics = this.getResources().getDisplayMetrics();
return metrics.widthPixels >= adWidthPx;
}
}
Any help will be greatly appreciated.
Thanks in advance!

PhoneGap works by throwing HTML/CSS/JS into a special WebView (CordovaWebView), which is running on the entire screen. The code that controls this default behavior lives inside their DroidApp class.
To get Millennial ads to work (or any standard Android controls, for that matter), you have to create a class that extends android.app.Activity (rather than DroidApp) and embed your CordovaWebView within that activity. That CordovaWebView will handle all of your app's Phonegap-related assets while your activity and its corresponding layout can handle your Android-specific classes (like Millennial's MMAdView).
To get a deeper understanding of how PhoneGap works on Android, I strongly recommend reading their docs here: http://cordova.apache.org/docs/en/2.7.0/guide_cordova-webview_android.md.html
To get more on how to specifically get Millennial's SDK working in that environment, talk to the support folks here: https://tools.mmedia.com/user/supportDevPortal

Related

Facebook Android SDK warning: "Removal of Tokenless Access Point"

I received the following warning via email from Facebook Developers a few days ago:
As part of our Graph API 8.0 release, we announced changes to how
developers can access User Picture and FB/IG OEmbed endpoints. We are
including more information about these changes below so you can make
the appropriate updates to your apps.
Facebook will now require client or app access tokens to access a
user’s profile picture when querying against user IDs. Beginning on
October 24, 2020, queries for profile pictures made against user IDs
without an access token will return a generic silhouette rather than a
profile picture. While client or app tokens will be required for user
ID queries, they will continue to be a best practice (and not
required) for ASID queries for the time being. You can read more about
this change in our developer documentation for User Picture.
Going by the above message, I'm not sure whether my block of code will be affected. Here is the code:
val profilePicUrl = Profile.getCurrentProfile().getProfilePictureUri(600, 600).toString()
That line gets the URI of the user's profile image after they have logged in.
Source code from the SDK:
/**
* Getter for the profile that is currently logged in to the application.
* #return The profile that is currently logged in to the application.
*/
public static Profile getCurrentProfile()
{
return ProfileManager.getInstance().getCurrentProfile();
}
/**
* Getter for the Uri of the profile picture.
*
* #param width The desired width for the profile picture.
* #param height The desired height for the profile picture.
* #return The Uri of the profile picture.
*/
public Uri getProfilePictureUri(
int width,
int height) {
return ImageRequest.getProfilePictureUri(this.id, width, height);
}
public static Uri getProfilePictureUri(
String userId,
int width,
int height) {
Validate.notNullOrEmpty(userId, "userId");
width = Math.max(width, UNSPECIFIED_DIMENSION);
height = Math.max(height, UNSPECIFIED_DIMENSION);
if (width == UNSPECIFIED_DIMENSION && height == UNSPECIFIED_DIMENSION) {
throw new IllegalArgumentException("Either width or height must be greater than 0");
}
Uri.Builder builder =
Uri.parse(ServerProtocol.getGraphUrlBase())
.buildUpon()
.path(String.format(
Locale.US, PATH,
FacebookSdk.getGraphApiVersion(),
userId));
if (height != UNSPECIFIED_DIMENSION) {
builder.appendQueryParameter(HEIGHT_PARAM, String.valueOf(height));
}
if (width != UNSPECIFIED_DIMENSION) {
builder.appendQueryParameter(WIDTH_PARAM, String.valueOf(width));
}
builder.appendQueryParameter(MIGRATION_PARAM, MIGRATION_VALUE);
I'm not sure whether I will be affected as I don't specifically use the Graph API?
Is someone able to shed some light on this?

how to define SampleGattAttributes in the following code?

public final static UUID UUID_HEART_RATE_MEASUREMENT =
UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);
SampleGattAttributes, appears in red colour,means unresolved symbol. How to define this?
I hope this will work for you.
Add SampleGattAttributes class in your package given below and import class.
import java.util.HashMap;
public class SampleGattAttributes {
private static HashMap<String, String> attributes = new HashMap();
public static String HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb";
static {
// Sample Services.
attributes.put("0000180d-0000-1000-8000-00805f9b34fb", "Heart Rate Service");
// Sample Characteristics.
attributes.put(HEART_RATE_MEASUREMENT, "Heart Rate Measurement");
}
}
I found an example from the Official Google Archive or Official Android repository for the SampleGattAttributes
Here is the full example:
/*
* 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 com.example.android.bluetoothlegatt;
import java.util.HashMap;
/**
* This class includes a small subset of standard GATT attributes for demonstration purposes.
*/
public class SampleGattAttributes {
private static HashMap<String, String> attributes = new HashMap();
public static String HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb";
public static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";
static {
// Sample Services.
attributes.put("0000180d-0000-1000-8000-00805f9b34fb", "Heart Rate Service");
attributes.put("0000180a-0000-1000-8000-00805f9b34fb", "Device Information Service");
// Sample Characteristics.
attributes.put(HEART_RATE_MEASUREMENT, "Heart Rate Measurement");
attributes.put("00002a29-0000-1000-8000-00805f9b34fb", "Manufacturer Name String");
}
public static String lookup(String uuid, String defaultName) {
String name = attributes.get(uuid);
return name == null ? defaultName : name;
}
}

How to implement a PDF viewer that loads pages asynchronously

We need to allow users of our mobile app to browse a magazine with an experience that is fast, fluid and feels native to the platform (similar to iBooks/Google Books).
Some featurs we need are being able to see Thumbnails of the whole magazine, and searching for specific text.
The problem is that our magazines are over 140 pages long and we can’t force our users to have to fully download the whole ebook/PDF beforehand. We need pages to be loaded asynchronously, that is, to let users start reading without having to fully download the content.
I studied PDFKit for iOS however I didn’t find any mention in the documentation about downloading a PDF asynchronously.
Are there any solutions/libraries to implement this functionality on iOS and Android?
What you're looking for is called linearization and according to this answer.
The first object immediately after the %PDF-1.x header line shall
contain a dictionary key indicating the /Linearized property of the
file.
This overall structure allows a conforming reader to learn the
complete list of object addresses very quickly, without needing to
download the complete file from beginning to end:
The viewer can display the first page(s) very fast, before the
complete file is downloaded.
The user can click on a thumbnail page preview (or a link in the ToC
of the file) in order to jump to, say, page 445, immediately after the
first page(s) have been displayed, and the viewer can then request all
the objects required for page 445 by asking the remote server via byte
range requests to deliver these "out of order" so the viewer can
display this page faster. (While the user reads pages out of order,
the downloading of the complete document will still go on in the
background...)
You can use this native library to linearization a PDF.
However
I wouldn't recommend made it has rendering the PDFs wont be fast, fluid or feel native. For those reasons, as far as I know there is no native mobile app that does linearization. Moreover, you have to create your own rendering engine for the PDF as most PDF viewing libraries do not support linearization . What you should do instead is convert the each individual page in the PDF to HTML on the server end and have the client only load the pages when required and cache. We will also save PDFs plan text separately in order to enable search. This way everything will be smooth as the resources will be lazy loaded. In order to achieve this you can do the following.
Firstly
On the server end, whenever you publish a PDF, the pages of the PDF should be split into HTML files as explained above. Page thumbs should also be generated from those pages. Assuming that your server is running on python with a flask microframework this is what you do.
from flask import Flask,request
from werkzeug import secure_filename
import os
from pyPdf import PdfFileWriter, PdfFileReader
import imgkit
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.pdfpage import PDFPage
from pdfminer.converter import XMLConverter, HTMLConverter, TextConverter
from pdfminer.layout import LAParams
import io
import sqlite3
import Image
app = Flask(__name__)
#app.route('/publish',methods=['GET','POST'])
def upload_file():
if request.method == 'POST':
f = request.files['file']
filePath = "pdfs/"+secure_filename(f.filename)
f.save(filePath)
savePdfText(filePath)
inputpdf = PdfFileReader(open(filePath, "rb"))
for i in xrange(inputpdf.numPages):
output = PdfFileWriter()
output.addPage(inputpdf.getPage(i))
with open("document-page%s.pdf" % i, "wb") as outputStream:
output.write(outputStream)
imgkit.from_file("document-page%s.pdf" % i, "document-page%s.jpg" % i)
saveThum("document-page%s.jpg" % i)
os.system("pdf2htmlEX --zoom 1.3 pdf/"+"document-page%s.pdf" % i)
def saveThum(infile):
save = 124,124
outfile = os.path.splitext(infile)[0] + ".thumbnail"
if infile != outfile:
try:
im = Image.open(infile)
im.thumbnail(size, Image.ANTIALIAS)
im.save(outfile, "JPEG")
except IOError:
print("cannot create thumbnail for '%s'" % infile)
def savePdfText(data):
fp = open(data, 'rb')
rsrcmgr = PDFResourceManager()
retstr = io.StringIO()
codec = 'utf-8'
laparams = LAParams()
device = TextConverter(rsrcmgr, retstr, codec=codec, laparams=laparams)
# Create a PDF interpreter object.
interpreter = PDFPageInterpreter(rsrcmgr, device)
# Process each page contained in the document.
db = sqlite3.connect("pdfText.db")
cursor = db.cursor()
cursor.execute('create table if not exists pagesTextTables(id INTEGER PRIMARY KEY,pageNum TEXT,pageText TEXT)')
db.commit()
pageNum = 1
for page in PDFPage.get_pages(fp):
interpreter.process_page(page)
data = retstr.getvalue()
cursor.execute('INSERT INTO pagesTextTables(pageNum,pageText) values(?,?) ',(str(pageNum),data ))
db.commit()
pageNum = pageNum+1
#app.route('/page',methods=['GET','POST'])
def getPage():
if request.method == 'GET':
page_num = request.files['page_num']
return send_file("document-page%s.html" % page_num, as_attachment=True)
#app.route('/thumb',methods=['GET','POST'])
def getThum():
if request.method == 'GET':
page_num = request.files['page_num']
return send_file("document-page%s.thumbnail" % page_num, as_attachment=True)
#app.route('/search',methods=['GET','POST'])
def search():
if request.method == 'GET':
query = request.files['query ']
db = sqlite3.connect("pdfText.db")
cursor = db.cursor()
cursor.execute("SELECT * from pagesTextTables Where pageText LIKE '%"+query +"%'")
result = cursor.fetchone()
response = Response()
response.headers['queryResults'] = result
return response
Here is an explanation of what the flask app is doing.
The /publish route is responsible for the publishing of your magazine, turning very page to HTML, saving the PDFs text to an SQlite db and generating thumbnails for those pages. I've used pyPDF for splitting the PDF to individual pages, pdfToHtmlEx to convert the pages to HTML, imgkit to generate those HTML to images and PIL to generate thumbs from those images. Also, a simple Sqlite db saves the pages' text.
The /page, /thumb and /search routes are self explanatory. They simply return the HTML, thumb or search query results.
Secondly, on the client end you simply download the HTML page whenever the user scrolls to it. Let me give you an example for android OS. Firstly, you'd want to Create some Utils to handle the GET requestrs
public static byte[] GetPage(int mPageNum){
return CallServer("page","page_num",Integer.toString(mPageNum))
}
public static byte[] GetThum(int mPageNum){
return CallServer("thumb","page_num",Integer.toString(mPageNum))
}
private static byte[] CallServer(String route,String requestName,String requestValue) throws IOException{
OkHttpClient client = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).build();
MultipartBody.Builder mMultipartBody = new MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart(requestName,requestValue);
RequestBody mRequestBody = mMultipartBody.build();
Request request = new Request.Builder()
.url("yourUrl/"+route).post(mRequestBody)
.build();
Response response = client.newCall(request).execute();
return response.body().bytes();
}
The helper utils above simple handle the queries to the server for you, they should be self explanatory.
Next, you simple create an RecyclerView with a WebView viewHolder or better yet an advanced webview as it will give you more power with customization.
public static class ViewHolder extends RecyclerView.ViewHolder {
private AdvancedWebView mWebView;
public ViewHolder(View itemView) {
super(itemView);
mWebView = (AdvancedWebView)itemView;}
}
private class ContentAdapter extends RecyclerView.Adapter<YourFrament.ViewHolder>{
#Override
public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
return new ViewHolder(new AdvancedWebView(container.getContext()));
}
#Override
public int getItemViewType(int position) {
return 0;
}
#Override
public void onBindViewHolder( ViewHolder holder, int position) {
handlePageDownload(holder.mWebView);
}
private void handlePageDownload(AdvancedWebView mWebView){....}
#Override
public int getItemCount() {
return numberOfPages;
}
}
That should be about it.
I am sorry to say, But there is no any library or SDK available which provides asynchronously pages loading functionality. It is next to impossible on the mobile device to open PDF file without downloading the full pdf file.
Solution:
I have already done R&D for the same and fulfilled your requirement in the project. I am not sure iBooks and Google books used below mechanism or not. But is working fine as per your requirements.
Divide your pdf into n number of part (E.g Suppose you have 150 pages in pdf then every pdf contain 15 pages -> It will take some effort from web end.)
Once first part download successfully then display it to the user and other part downloading asynchronously.
After downloading all part of the pdf file, Use below code the merge Pdf file.
How to Merge PDF file
UIGraphicsBeginPDFContextToFile(oldFile, paperSize, nil);
for (pageNumber = 1; pageNumber <= count; pageNumber++)
{
UIGraphicsBeginPDFPageWithInfo(paperSize, nil);
//Get graphics context to draw the page
CGContextRef currentContext = UIGraphicsGetCurrentContext();
//Flip and scale context to draw the pdf correctly
CGContextTranslateCTM(currentContext, 0, paperSize.size.height);
CGContextScaleCTM(currentContext, 1.0, -1.0);
//Get document access of the pdf from which you want a page
CGPDFDocumentRef newDocument = CGPDFDocumentCreateWithURL ((CFURLRef) newUrl);
//Get the page you want
CGPDFPageRef newPage = CGPDFDocumentGetPage (newDocument, pageNumber);
//Drawing the page
CGContextDrawPDFPage (currentContext, newPage);
//Clean up
newPage = nil;
CGPDFDocumentRelease(newDocument);
newDocument = nil;
newUrl = nil;
}
UIGraphicsEndPDFContext();
Reference: How to merge PDF file.
Update:
Main advantage of this mechanism is Logic remain same for all device Android and iOS Device.

OSMdroid change Tiles cache in storage

Sorry for my bad English, i have a question. I'm new in android and don't know how to do this.
How can i modify OSMdroid 4.2 jar file codes in my project?
I added these jar files as library to my project. My app in 3 languages. I have tile source for every language. When I select language, program selects a tile source with the same language. But when i zoom map, automatically language changes in map. I don't know why it do this. I think it depends on tiles cach in storage.
Is there any way to change tile cache in storage for every language individually?
Thanks! :)
See: https://github.com/osmdroid/osmdroid/issues/78
This is the same issue (OSMDROID_PATH and TILE_PATH_BASE hardcoded and final).
Using this patch (potentially old version below), you could change the tile cache path as needed.
package org.osmdroid.tileprovider.constants;
import java.io.File;
import android.os.Environment;
/**
* This class contains key settings related to the osmdroid cache, and methods to change default values.
*/
public class TilesCacheSettings {
/** Base path for osmdroid files. Zip files are in this folder. */
public static File OSMDROID_PATH = new File(Environment.getExternalStorageDirectory(),
"osmdroid");
/** Base path for tiles. */
public static File TILE_PATH_BASE = new File(OSMDROID_PATH, "tiles");
/** 600 Mb */
public static long TILE_MAX_CACHE_SIZE_BYTES = 600L * 1024 * 1024;
/** 500 Mb */
public static long TILE_TRIM_CACHE_SIZE_BYTES = 500L * 1024 * 1024;
/** Change the root path of the osmdroid cache.
* By default, it is defined in SD card, osmdroid directory.
* #param newFullPath
*/
public static void setCachePath(String newFullPath){
OSMDROID_PATH = new File(newFullPath);
TILE_PATH_BASE = new File(OSMDROID_PATH, "tiles");
}
/** Change the osmdroid tiles cache sizes
* #param maxCacheSize in Mb. Default is 600 Mb.
* #param trimCacheSize When the cache size exceeds maxCacheSize, tiles will be automatically removed to reach this target. In Mb. Default is 500 Mb.
*/
public static void setCacheSizes(long maxCacheSize, long trimCacheSize){
TILE_MAX_CACHE_SIZE_BYTES = maxCacheSize * 1024 * 1024;
TILE_TRIM_CACHE_SIZE_BYTES = trimCacheSize * 1024 * 1024;
}
}
Only add the following code in your
OpenStreetMapTileProviderConstants.setCachePath(this.getFilesDir().getAbsolutePath());
or
OpenStreetMapTileProviderConstants.setCachePath("other path");

Simple hiding/obfuscation of strings in APK?

Sometimes you need to store a password in the app itself, such as a username/password for communicating with your own server. In these cases it's not possible to follow the normal process of storing passwords - i.e. hash the password, store the hash, compare to hashed user input - because you don't have any user input to compare the hash to. The password needs to be provided by the app itself. So how to protect the stored password in the APK? Would a password-generating function like the one below be reasonably secure?
Plain text:
String password = "$()&HDI?=!";
Simple obfuscation:
private String getPassword(){
String pool = "%&/#$()7?=!656sd8KJ%&HDI!!!G98y/&%=?=*^%&ft4%(";
return pool.substring(4, 7) + pool.substring(20, 24) + pool.substring(8, 11);
}
I know ProGuard has some obfuscation capabilities, but I'm curious about what the above "obfuscation" technique does when it's compiled, and how hard it would be for someone to figure it out by looking in the APK and/or using other more sophisticated techniques?
tl;dr If you know how to decompile APK, you can easily get the password, no matter how obfuscated the code was. Don't store passwords in APK, it's not secure.
I know ProGuard has some obfuscation capabilities, but I'm curious
about what the above "obfuscation" technique does when it's compiled,
and how hard it would be for someone to figure it out by looking in
the APK and/or using other more sophisticated techniques?
I will show you how easy it is. Here is an Android SSCCE which we will decompile:
MyActivity.java:
public class MyActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
TextView text = (TextView) findViewById(R.id.text);
text.setText(getPassword());
}
private String getPassword() {
String pool = "%&/#$()7?=!656sd8KJ%&HDI!!!G98y/&%=?=*^%&ft4%(";
return pool.substring(4, 7) + pool.substring(20, 24) + pool.substring(8, 11);
}
}
main.xml:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
After compiling it and running we can see $()&HDI?=! on a TextView.
Let's decompile an APK:
unzip myapp.apk or right-click on the APK and Unzip here.
classes.dex file appears.
Convert classes.dex to JAR file with dex2jar. After executing dex2jar.sh classes.dex, classes_dex2jar.jar file
appears.
Using some Java decompiler on classes_dex2jar.jar, for example JD-GUI, we retrieve such Java code from MyActivity.class:
public class MyActivity extends Activity
{
private String getPassword()
{
return "%&/#$()7?=!656sd8KJ%&HDI!!!G98y/&%=?=*^%&ft4%(".substring(4, 7)
+ "%&/#$()7?=!656sd8KJ%&HDI!!!G98y/&%=?=*^%&ft4%(".substring(20, 24)
+ "%&/#$()7?=!656sd8KJ%&HDI!!!G98y/&%=?=*^%&ft4%(".substring(8, 11);
}
public void onCreate(Bundle paramBundle)
{
super.onCreate(paramBundle);
setContentView(2130903040);
((TextView)findViewById(2131034112)).setText(getPassword());
}
}
ProGuard can't help much, the code will be still easily readable.
Based on the above, I can already give you an answer for this question:
Would a password-generating function like the one below be reasonably
secure?
No. As you can see, it increases difficulty of reading deobfuscated code by a tiny bit. We should not obfuscate the code in such way, because:
It gives an illusion of security.
It is a waste of developer's time.
It decreases readability of the code.
In the official Android documentation, in the Security and Design part, they're advising this to protect your Google Play public key:
To keep your public key safe from malicious users and hackers, do not
embed it in any code as a literal string. Instead, construct the
string at runtime from pieces or use bit manipulation (for example,
XOR with some other string) to hide the actual key. The key itself is
not secret information, but you do not want to make it easy for a
hacker or malicious user to replace the public key with another key.
Ok, so let's try that:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
TextView text = (TextView) findViewById(R.id.text);
text.setText(xor("A#NCyw&IHY", "ehge13ovux"));
}
private String xor(String a, String b) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < a.length() && i < b.length(); i++) {
sb.append((char) (a.charAt(i) ^ b.charAt(i)));
}
return sb.toString();
}
Gives $()&HDI?=! on a TextView, good.
Decompiled version:
public void onCreate(Bundle paramBundle)
{
super.onCreate(paramBundle);
setContentView(2130903040);
((TextView)findViewById(2131034112)).setText(xor("A#NCyw&IHY", "ehge13ovux"));
}
private String xor(String paramString1, String paramString2)
{
StringBuilder localStringBuilder = new StringBuilder();
for (int i = 0; (i < paramString1.length()) && (i < paramString2.length()); i++) {
localStringBuilder.append((char)(paramString1.charAt(i) ^ paramString2.charAt(i)));
}
return localStringBuilder.toString();
}
Very similar situation like before.
Even if we had extremely complicated function soStrongObfuscationOneGetsBlind(), we can always run decompiled code and see what is it producing. Or debug it step-by-step.

Categories

Resources