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