1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.smspush; 18 19 import android.app.Service; 20 import android.content.ActivityNotFoundException; 21 import android.content.ComponentName; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ApplicationInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.PackageManager.NameNotFoundException; 28 import android.database.Cursor; 29 import android.database.sqlite.SQLiteDatabase; 30 import android.database.sqlite.SQLiteOpenHelper; 31 import android.os.Build; 32 import android.os.IBinder; 33 import android.os.PowerManager; 34 import android.os.RemoteException; 35 import android.os.UserManager; 36 import android.util.Log; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.internal.telephony.IWapPushManager; 40 import com.android.internal.telephony.WapPushManagerParams; 41 42 import java.io.File; 43 44 /** 45 * The WapPushManager service is implemented to process incoming 46 * WAP Push messages and to maintain the Receiver Application/Application 47 * ID mapping. The WapPushManager runs as a system service, and only the 48 * WapPushManager can update the WAP Push message and Receiver Application 49 * mapping (Application ID Table). When the device receives an SMS WAP Push 50 * message, the WapPushManager looks up the Receiver Application name in 51 * Application ID Table. If an application is found, the application is 52 * launched using its full component name instead of broadcasting an implicit 53 * Intent. If a Receiver Application is not found in the Application ID 54 * Table or the WapPushManager returns a process-further value, the 55 * telephony stack will process the message using existing message processing 56 * flow, and broadcast an implicit Intent. 57 */ 58 public class WapPushManager extends Service { 59 60 private static final String LOG_TAG = "WAP PUSH"; 61 private static final String DATABASE_NAME = "wappush.db"; 62 private static final String APPID_TABLE_NAME = "appid_tbl"; 63 64 /** 65 * Version number must be incremented when table structure is changed. 66 */ 67 private static final int WAP_PUSH_MANAGER_VERSION = 1; 68 private static final boolean DEBUG_SQL = false; 69 private static final boolean LOCAL_LOGV = false; 70 71 /** 72 * Inner class that deals with application ID table 73 */ 74 @VisibleForTesting 75 public static class WapPushManDBHelper extends SQLiteOpenHelper { 76 /** 77 * Constructor 78 */ 79 @VisibleForTesting WapPushManDBHelper(Context context)80 public WapPushManDBHelper(Context context) { 81 super(context, DATABASE_NAME, null, WAP_PUSH_MANAGER_VERSION); 82 if (LOCAL_LOGV) Log.v(LOG_TAG, "helper instance created."); 83 } 84 85 @Override onCreate(SQLiteDatabase db)86 public void onCreate(SQLiteDatabase db) { 87 if (LOCAL_LOGV) Log.v(LOG_TAG, "db onCreate."); 88 String sql = "CREATE TABLE " + APPID_TABLE_NAME + " (" 89 + "id INTEGER PRIMARY KEY, " 90 + "x_wap_application TEXT, " 91 + "content_type TEXT, " 92 + "package_name TEXT, " 93 + "class_name TEXT, " 94 + "app_type INTEGER, " 95 + "need_signature INTEGER, " 96 + "further_processing INTEGER, " 97 + "install_order INTEGER " 98 + ")"; 99 100 if (DEBUG_SQL) Log.v(LOG_TAG, "sql: " + sql); 101 db.execSQL(sql); 102 } 103 104 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)105 public void onUpgrade(SQLiteDatabase db, 106 int oldVersion, int newVersion) { 107 // TODO: when table structure is changed, need to dump and restore data. 108 /* 109 db.execSQL( 110 "drop table if exists "+APPID_TABLE_NAME); 111 onCreate(db); 112 */ 113 Log.w(LOG_TAG, "onUpgrade is not implemented yet. do nothing."); 114 } 115 116 protected class queryData { 117 public String packageName; 118 public String className; 119 int appType; 120 int needSignature; 121 int furtherProcessing; 122 int installOrder; 123 } 124 125 /** 126 * Query the latest receiver application info with supplied application ID and 127 * content type. 128 * @param app_id application ID to look up 129 * @param content_type content type to look up 130 */ queryLastApp(SQLiteDatabase db, String app_id, String content_type)131 protected queryData queryLastApp(SQLiteDatabase db, 132 String app_id, String content_type) { 133 if (LOCAL_LOGV) Log.v(LOG_TAG, "queryLastApp app_id: " + app_id 134 + " content_type: " + content_type); 135 136 Cursor cur = db.query(APPID_TABLE_NAME, 137 new String[] {"install_order", "package_name", "class_name", 138 "app_type", "need_signature", "further_processing"}, 139 "x_wap_application=? and content_type=?", 140 new String[] {app_id, content_type}, 141 null /* groupBy */, 142 null /* having */, 143 "install_order desc" /* orderBy */); 144 145 queryData ret = null; 146 147 if (cur.moveToNext()) { 148 ret = new queryData(); 149 ret.installOrder = cur.getInt(cur.getColumnIndex("install_order")); 150 ret.packageName = cur.getString(cur.getColumnIndex("package_name")); 151 ret.className = cur.getString(cur.getColumnIndex("class_name")); 152 ret.appType = cur.getInt(cur.getColumnIndex("app_type")); 153 ret.needSignature = cur.getInt(cur.getColumnIndex("need_signature")); 154 ret.furtherProcessing = cur.getInt(cur.getColumnIndex("further_processing")); 155 } 156 cur.close(); 157 return ret; 158 } 159 160 } 161 162 /** 163 * The exported API implementations class 164 */ 165 private class IWapPushManagerStub extends IWapPushManager.Stub { 166 public Context mContext; 167 IWapPushManagerStub()168 public IWapPushManagerStub() { 169 170 } 171 172 /** 173 * Compare the package signature with WapPushManager package 174 */ signatureCheck(String package_name)175 protected boolean signatureCheck(String package_name) { 176 PackageManager pm = mContext.getPackageManager(); 177 int match = pm.checkSignatures(mContext.getPackageName(), package_name); 178 179 if (LOCAL_LOGV) Log.v(LOG_TAG, "compare signature " + mContext.getPackageName() 180 + " and " + package_name + ", match=" + match); 181 182 return match == PackageManager.SIGNATURE_MATCH; 183 } 184 185 /** 186 * Returns the status value of the message processing. 187 * The message will be processed as follows: 188 * 1.Look up Application ID Table with x-wap-application-id + content type 189 * 2.Check the signature of package name that is found in the 190 * Application ID Table by using PackageManager.checkSignature 191 * 3.Trigger the Application 192 * 4.Returns the process status value. 193 */ processMessage(String app_id, String content_type, Intent intent)194 public int processMessage(String app_id, String content_type, Intent intent) 195 throws RemoteException { 196 Log.d(LOG_TAG, "wpman processMsg " + app_id + ":" + content_type); 197 198 WapPushManDBHelper dbh = getDatabase(mContext); 199 SQLiteDatabase db = dbh.getReadableDatabase(); 200 WapPushManDBHelper.queryData lastapp = dbh.queryLastApp(db, app_id, content_type); 201 db.close(); 202 203 if (lastapp == null) { 204 Log.w(LOG_TAG, "no receiver app found for " + app_id + ":" + content_type); 205 return WapPushManagerParams.APP_QUERY_FAILED; 206 } 207 if (LOCAL_LOGV) Log.v(LOG_TAG, "starting " + lastapp.packageName 208 + "/" + lastapp.className); 209 210 if (lastapp.needSignature != 0) { 211 if (!signatureCheck(lastapp.packageName)) { 212 return WapPushManagerParams.SIGNATURE_NO_MATCH; 213 } 214 } 215 216 if (lastapp.appType == WapPushManagerParams.APP_TYPE_ACTIVITY) { 217 //Intent intent = new Intent(Intent.ACTION_MAIN); 218 intent.setClassName(lastapp.packageName, lastapp.className); 219 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 220 221 try { 222 mContext.startActivity(intent); 223 } catch (ActivityNotFoundException e) { 224 Log.w(LOG_TAG, "invalid name " + 225 lastapp.packageName + "/" + lastapp.className); 226 return WapPushManagerParams.INVALID_RECEIVER_NAME; 227 } 228 } else { 229 intent.setClassName(mContext, lastapp.className); 230 intent.setComponent(new ComponentName(lastapp.packageName, 231 lastapp.className)); 232 PackageManager pm = mContext.getPackageManager(); 233 PowerManager powerManager = 234 (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 235 try { 236 ApplicationInfo appInfo = pm.getApplicationInfo(lastapp.packageName, 0); 237 if (appInfo.targetSdkVersion < Build.VERSION_CODES.O || 238 powerManager.isIgnoringBatteryOptimizations(lastapp.packageName)) { 239 if (mContext.startService(intent) == null) { 240 Log.w(LOG_TAG, "invalid name " + 241 lastapp.packageName + "/" + lastapp.className); 242 return WapPushManagerParams.INVALID_RECEIVER_NAME; 243 } 244 } else { 245 if (mContext.startForegroundService(intent) == null) { 246 Log.w(LOG_TAG, "invalid name " + 247 lastapp.packageName + "/" + lastapp.className); 248 return WapPushManagerParams.INVALID_RECEIVER_NAME; 249 } 250 } 251 252 } catch (NameNotFoundException e) { 253 Log.w(LOG_TAG, "invalid name " + 254 lastapp.packageName + "/" + lastapp.className); 255 return WapPushManagerParams.INVALID_RECEIVER_NAME; 256 } 257 } 258 259 return WapPushManagerParams.MESSAGE_HANDLED 260 | (lastapp.furtherProcessing == 1 ? 261 WapPushManagerParams.FURTHER_PROCESSING : 0); 262 } 263 appTypeCheck(int app_type)264 protected boolean appTypeCheck(int app_type) { 265 if (app_type == WapPushManagerParams.APP_TYPE_ACTIVITY || 266 app_type == WapPushManagerParams.APP_TYPE_SERVICE) { 267 return true; 268 } else { 269 return false; 270 } 271 } 272 273 /** 274 * Returns true if adding the package succeeded. 275 */ addPackage(String x_app_id, String content_type, String package_name, String class_name, int app_type, boolean need_signature, boolean further_processing)276 public boolean addPackage(String x_app_id, String content_type, 277 String package_name, String class_name, 278 int app_type, boolean need_signature, boolean further_processing) { 279 WapPushManDBHelper dbh = getDatabase(mContext); 280 SQLiteDatabase db = dbh.getWritableDatabase(); 281 282 if (!appTypeCheck(app_type)) { 283 Log.w(LOG_TAG, "invalid app_type " + app_type + ". app_type must be " 284 + WapPushManagerParams.APP_TYPE_ACTIVITY + " or " 285 + WapPushManagerParams.APP_TYPE_SERVICE); 286 return false; 287 } 288 boolean ret = insertPackage(dbh, db, x_app_id, content_type, package_name, class_name, 289 app_type, need_signature, further_processing); 290 db.close(); 291 292 return ret; 293 } 294 295 /** 296 * Returns true if updating the package succeeded. 297 */ updatePackage(String x_app_id, String content_type, String package_name, String class_name, int app_type, boolean need_signature, boolean further_processing)298 public boolean updatePackage(String x_app_id, String content_type, 299 String package_name, String class_name, 300 int app_type, boolean need_signature, boolean further_processing) { 301 302 if (!appTypeCheck(app_type)) { 303 Log.w(LOG_TAG, "invalid app_type " + app_type + ". app_type must be " 304 + WapPushManagerParams.APP_TYPE_ACTIVITY + " or " 305 + WapPushManagerParams.APP_TYPE_SERVICE); 306 return false; 307 } 308 309 WapPushManDBHelper dbh = getDatabase(mContext); 310 SQLiteDatabase db = dbh.getWritableDatabase(); 311 WapPushManDBHelper.queryData lastapp = dbh.queryLastApp(db, x_app_id, content_type); 312 313 if (lastapp == null) { 314 db.close(); 315 return false; 316 } 317 318 ContentValues values = new ContentValues(); 319 String where = "x_wap_application=\'" + x_app_id + "\'" 320 + " and content_type=\'" + content_type + "\'" 321 + " and install_order=" + lastapp.installOrder; 322 323 values.put("package_name", package_name); 324 values.put("class_name", class_name); 325 values.put("app_type", app_type); 326 values.put("need_signature", need_signature ? 1 : 0); 327 values.put("further_processing", further_processing ? 1 : 0); 328 329 int num = db.update(APPID_TABLE_NAME, values, where, null); 330 if (LOCAL_LOGV) Log.v(LOG_TAG, "update:" + x_app_id + ":" + content_type + " " 331 + package_name + "." + class_name 332 + ", sq:" + lastapp.installOrder); 333 334 db.close(); 335 336 return num > 0; 337 } 338 339 /** 340 * Returns true if deleting the package succeeded. 341 */ deletePackage(String x_app_id, String content_type, String package_name, String class_name)342 public boolean deletePackage(String x_app_id, String content_type, 343 String package_name, String class_name) { 344 WapPushManDBHelper dbh = getDatabase(mContext); 345 SQLiteDatabase db = dbh.getWritableDatabase(); 346 String where = "x_wap_application=\'" + x_app_id + "\'" 347 + " and content_type=\'" + content_type + "\'" 348 + " and package_name=\'" + package_name + "\'" 349 + " and class_name=\'" + class_name + "\'"; 350 int num_removed = db.delete(APPID_TABLE_NAME, where, null); 351 352 db.close(); 353 if (LOCAL_LOGV) Log.v(LOG_TAG, "deleted " + num_removed + " rows:" 354 + x_app_id + ":" + content_type + " " 355 + package_name + "." + class_name); 356 return num_removed > 0; 357 } 358 }; 359 360 361 /** 362 * Linux IPC Binder 363 */ 364 private final IWapPushManagerStub mBinder = new IWapPushManagerStub(); 365 366 /** 367 * Default constructor 368 */ WapPushManager()369 public WapPushManager() { 370 super(); 371 mBinder.mContext = this; 372 } 373 374 @Override onBind(Intent arg0)375 public IBinder onBind(Intent arg0) { 376 return mBinder; 377 } 378 379 /** 380 * Application ID database instance 381 */ 382 private WapPushManDBHelper mDbHelper = null; getDatabase(Context context)383 protected WapPushManDBHelper getDatabase(Context context) { 384 if (mDbHelper == null) { 385 if (LOCAL_LOGV) Log.v(LOG_TAG, "create new db inst."); 386 mDbHelper = new WapPushManDBHelper(context.createDeviceProtectedStorageContext()); 387 } 388 // Migrate existing legacy database into the device encrypted storage. 389 migrateWapPushManDBIfNeeded(context); 390 return mDbHelper; 391 } 392 393 /** 394 * Inserts a package information into a database 395 */ 396 @VisibleForTesting insertPackage(WapPushManDBHelper dbh, SQLiteDatabase db, String appId, String contentType, String packageName, String className, int appType, boolean needSignature, boolean furtherProcessing)397 public boolean insertPackage(WapPushManDBHelper dbh, SQLiteDatabase db, String appId, 398 String contentType, String packageName, String className, int appType, 399 boolean needSignature, boolean furtherProcessing) { 400 401 WapPushManDBHelper.queryData lastapp = dbh.queryLastApp(db, appId, contentType); 402 boolean insert = false; 403 int sq = 0; 404 405 if (lastapp == null) { 406 insert = true; 407 sq = 0; 408 } else if (!lastapp.packageName.equals(packageName) 409 || !lastapp.className.equals(className)) { 410 insert = true; 411 sq = lastapp.installOrder + 1; 412 } 413 414 if (insert) { 415 ContentValues values = new ContentValues(); 416 417 values.put("x_wap_application", appId); 418 values.put("content_type", contentType); 419 values.put("package_name", packageName); 420 values.put("class_name", className); 421 values.put("app_type", appType); 422 values.put("need_signature", needSignature ? 1 : 0); 423 values.put("further_processing", furtherProcessing ? 1 : 0); 424 values.put("install_order", sq); 425 db.insert(APPID_TABLE_NAME, null, values); 426 if (LOCAL_LOGV) { 427 Log.v(LOG_TAG, "add:" + appId + ":" + contentType + " " + packageName 428 + "." + className + ", newsq:" + sq); 429 } 430 return true; 431 } 432 return false; 433 } 434 435 /** 436 * Migrates a legacy database into the device encrypted storage 437 */ migrateWapPushManDBIfNeeded(Context context)438 private void migrateWapPushManDBIfNeeded(Context context) { 439 UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); 440 File file = context.getDatabasePath(DATABASE_NAME); 441 if (!userManager.isUserUnlocked() || !file.exists()) { 442 // Check if the device is unlocked because a legacy database can't access during 443 // DirectBoot. 444 return; 445 } 446 447 // Migration steps below: 448 // 1. Merge the package info to legacy database if there is any package info which is 449 // registered during DirectBoot. 450 // 2. Move the data base to the device encryped storage. 451 WapPushManDBHelper legacyDbHelper = new WapPushManDBHelper(context); 452 SQLiteDatabase legacyDb = legacyDbHelper.getWritableDatabase(); 453 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 454 Cursor cur = db.query(APPID_TABLE_NAME, null, null, null, null, null, null); 455 while (cur.moveToNext()) { 456 insertPackage(legacyDbHelper, legacyDb, 457 cur.getString(cur.getColumnIndex("x_wap_application")), 458 cur.getString(cur.getColumnIndex("content_type")), 459 cur.getString(cur.getColumnIndex("package_name")), 460 cur.getString(cur.getColumnIndex("class_name")), 461 cur.getInt(cur.getColumnIndex("app_type")), 462 cur.getInt(cur.getColumnIndex("need_signature")) == 1, 463 cur.getInt(cur.getColumnIndex("further_processing")) == 1); 464 } 465 cur.close(); 466 legacyDb.close(); 467 db.close(); 468 context.createDeviceProtectedStorageContext().moveDatabaseFrom(context, DATABASE_NAME); 469 Log.i(LOG_TAG, "Migrated the legacy database."); 470 } 471 472 /** 473 * This method is used for testing 474 */ verifyData(String x_app_id, String content_type, String package_name, String class_name, int app_type, boolean need_signature, boolean further_processing)475 public boolean verifyData(String x_app_id, String content_type, 476 String package_name, String class_name, 477 int app_type, boolean need_signature, boolean further_processing) { 478 WapPushManDBHelper dbh = getDatabase(this); 479 SQLiteDatabase db = dbh.getReadableDatabase(); 480 WapPushManDBHelper.queryData lastapp = dbh.queryLastApp(db, x_app_id, content_type); 481 482 if (LOCAL_LOGV) Log.v(LOG_TAG, "verifyData app id: " + x_app_id + " content type: " + 483 content_type + " lastapp: " + lastapp); 484 485 db.close(); 486 487 if (lastapp == null) return false; 488 489 if (LOCAL_LOGV) Log.v(LOG_TAG, "verifyData lastapp.packageName: " + lastapp.packageName + 490 " lastapp.className: " + lastapp.className + 491 " lastapp.appType: " + lastapp.appType + 492 " lastapp.needSignature: " + lastapp.needSignature + 493 " lastapp.furtherProcessing: " + lastapp.furtherProcessing); 494 495 496 if (lastapp.packageName.equals(package_name) 497 && lastapp.className.equals(class_name) 498 && lastapp.appType == app_type 499 && lastapp.needSignature == (need_signature ? 1 : 0) 500 && lastapp.furtherProcessing == (further_processing ? 1 : 0)) { 501 return true; 502 } else { 503 return false; 504 } 505 } 506 507 /** 508 * This method is used for testing 509 */ isDataExist(String x_app_id, String content_type, String package_name, String class_name)510 public boolean isDataExist(String x_app_id, String content_type, 511 String package_name, String class_name) { 512 WapPushManDBHelper dbh = getDatabase(this); 513 SQLiteDatabase db = dbh.getReadableDatabase(); 514 boolean ret = dbh.queryLastApp(db, x_app_id, content_type) != null; 515 516 db.close(); 517 return ret; 518 } 519 520 } 521 522