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