Commit 0b0dd23a by Kim Peace

Merge commit '96f79ab9' into features/1.3.101

# Conflicts:
#	ABVJE_Launcher_Android/assets/chat
parents 3fd5430d 96f79ab9
......@@ -16,8 +16,8 @@ public class MediaInfoJSON extends AbstractJSON {
public static final int BUTTON_TYPE = 1;
public static final int VIDEO_TYPE = 2;
public static final int MUSIC_TYPE = 3;
public static final int CHANGE_IMAGE_TYPE = 4;
public static final int CHANGE_VIDEO_TYPE = 5;
public static final int CHANGE_IMAGE_TYPE = 4; // 差し替え画像
public static final int CHANGE_VIDEO_TYPE = 5; // 差し替え動画
public static final int TRIGGER_TYPE = 6;
public static final int RICH_TEXT_TYPE = 7;
public static final int VIEW3D_TYPE = 8;
......
......@@ -156,4 +156,6 @@ public class ABookKeys {
public static final String APPROVAL_LIST = "approvalList";
public static final String PHASE_LIST = "phaseList";
public static final String PROCESS_LIST = "processList";
public static final String MAIL_TO_URL = "mailto";
}
......@@ -23,6 +23,8 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- GCM -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
......@@ -49,7 +51,8 @@
android:allowBackup="false"
tools:replace="android:allowBackup"
android:usesCleartextTraffic="true"
android:largeHeap="true" >
android:largeHeap="true"
android:requestLegacyExternalStorage="true">
<service android:name="jp.agentec.abook.abv.cl.push.ABVFcmListenerService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"></action>
......@@ -62,6 +65,11 @@
</intent-filter>
</service>
<service
android:name="jp.agentec.abook.abv.launcher.android.BackgroundDownloadService"
android:enabled="true"
android:exported="false" />
<receiver android:name="jp.agentec.abook.abv.launcher.android.OnAppDownloadReceiver">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED" />
......@@ -149,42 +157,40 @@
android:name="jp.agentec.abook.abv.ui.home.activity.LoginActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="LoginActivity"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" />
<activity
android:name="jp.agentec.abook.abv.ui.viewer.activity.ContentViewActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:hardwareAccelerated="false"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" />
<activity
android:name="jp.agentec.abook.abv.ui.viewer.activity.HTMLWebViewActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar" >
android:configChanges="keyboardHidden|orientation|screenSize">
</activity>
<activity
android:name="jp.agentec.abook.abv.ui.viewer.activity.HTMLXWalkWebViewActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar" >
android:configChanges="keyboardHidden|orientation|screenSize">
</activity>
<activity
android:name="jp.agentec.abook.abv.ui.viewer.activity.EnqueteWebViewActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar" >
>
</activity>
<activity
android:name="jp.agentec.abook.abv.ui.viewer.activity.PreviewActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" >
</activity>
<activity
android:name="jp.agentec.abook.abv.ui.viewer.activity.VideoViewActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" >
</activity>
<activity
android:name="jp.agentec.abook.abv.ui.home.activity.LoginPasswordChangeActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="LoginPasswordChangeActivity"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" >
</activity>
<activity
android:name="jp.agentec.abook.abv.ui.viewer.activity.DeviceImageListActivityDialog"
......@@ -193,19 +199,19 @@
android:theme="@style/Theme_Contentdetailview" />
<activity android:name="jp.agentec.abook.abv.ui.viewer.activity.DeviceImageListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/>
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen"/>
<activity android:name="jp.agentec.abook.abv.ui.viewer.activity.theta.ThetaCameraActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/>
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen"/>
<activity android:name="jp.agentec.abook.abv.ui.viewer.activity.theta.ThetaImageListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/>
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen"/>
<activity android:name="jp.agentec.abook.abv.ui.viewer.activity.theta.ThetaImagePreviewActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/>
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen"/>
<activity android:name="jp.agentec.abook.abv.ui.home.activity.ABookSettingActivity" android:theme="@android:style/Theme.NoTitleBar"/>
<activity android:name="jp.agentec.abook.abv.ui.home.activity.ABookSettingActivity" android:theme="@style/Theme.AppCompat.NoActionBar"/>
<activity android:name="jp.agentec.abook.abv.ui.home.activity.ChangePasswordActivity" android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/>
<activity android:name="jp.agentec.abook.abv.ui.home.activity.ChangePasswordActivityDialog" android:theme="@style/Theme.MyTheme.ModalDialog"/>
<activity android:name="jp.agentec.abook.abv.ui.home.activity.HelpActivity" android:configChanges="orientation|screenSize"/>
......@@ -215,11 +221,11 @@
<activity android:name="jp.agentec.abook.abv.ui.viewer.activity.AudioPlayActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" />
<activity android:name="jp.agentec.abook.abv.ui.viewer.activity.ImageViewActivity"
android:hardwareAccelerated="false"
android:configChanges="keyboardHidden"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" />
<activity android:name="jp.agentec.abook.abv.ui.common.activity.ShowPushMessageDailogActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
......@@ -240,7 +246,7 @@
<activity
android:name="jp.agentec.abook.abv.ui.viewer.activity.OnlineHTMLWebViewActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" >
</activity>
<activity android:name="jp.agentec.abook.abv.ui.home.activity.ChatWebviewActivity" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
......
Subproject commit e744ad70c71280f0bb1e0fba333fd6bfdb91721e
Subproject commit 515a1461c829fcc0a8a7aca957d2ce730e317ce0
......@@ -17,11 +17,12 @@ dependencies {
}
android {
compileSdkVersion 26
compileSdkVersion 29
buildToolsVersion '28.0.3'
defaultConfig {
minSdkVersion 23
targetSdkVersion 29
multiDexEnabled true
// from gradle.properties
......
......@@ -1483,5 +1483,6 @@
<string name="msg_invite_collaboration">協業に招待されました。</string>
<!-- 連続作業 -->
<string name="msg_error_all_process_delete">全削除の送信に失敗しました。</string>
<string name="msg_ozd_file_could_not_opened">帳票ファイルを開くことができませんでした。</string>
</resources>
......@@ -1486,6 +1486,7 @@
<string name="msg_error_chat_room_sc_forbidden">사용자 정보를 확인할 수 없습니다. 다시 로그인하시기 바랍니다.</string>
<string name="msg_error_all_process_delete">모두 삭제 송신에 실패하였습니다.</string>
<string name="msg_error_already_exist_same_room">동일한 채팅방이 이미 존재합니다.</string>
<string name="msg_ozd_file_could_not_opened">장표 파일을 열 수 없습니다.</string>
<string name="msg_confirm_send_host_change">방장 권한을 전달 받으시겠습니까?</string>
<string name="msg_invite_collaboration">협업에 초대되셨습니다.</string>
</resources>
\ No newline at end of file
......@@ -1483,6 +1483,7 @@
<string name="msg_error_chat_room_sc_forbidden">Failed to authenticate. Please login again.</string>
<string name="msg_error_all_process_delete">Failed to send all deletes.</string>
<string name="msg_error_already_exist_same_room">Already exist same room.</string>
<string name="msg_ozd_file_could_not_opened">Report file could not opened.</string>
<string name="msg_confirm_send_host_change">Do you want to receive host permissions?</string>
<string name="msg_invite_collaboration">You are invited to collaboration.</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--The outline turns to colorPrimary when the EditText is in focus-->
<item android:state_enabled="true" android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="1dp" />
<stroke android:width="3dp" android:color="@android:color/holo_orange_light" />
<padding
android:bottom="7dp"
android:left="7dp"
android:right="7dp"
android:top="7dp" />
</shape>
</item>
<!--The outline turns to grey when the EditText is out of focus-->
<item android:state_enabled="true">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="1dp" />
<stroke android:width="1dp" android:color="@android:color/white" />
<padding
android:bottom="7dp"
android:left="7dp"
android:right="7dp"
android:top="7dp" />
</shape>
</item>
</selector>
\ No newline at end of file
......@@ -20,6 +20,7 @@
android:layout_weight="1"
android:imeActionLabel="@string/search"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLength="30"
android:padding="10dp"
android:maxLines="1" />
......
......@@ -49,16 +49,17 @@
android:visibility="gone"
android:textStyle="bold" />
</LinearLayout>
<Button
<ImageButton
android:id="@+id/btn_operation_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="15dp"
android:visibility="gone"
android:background="@drawable/ic_operation_home"
android:contentDescription="@string/cont_desc" />
android:contentDescription="@string/cont_desc"
android:visibility="gone" />
<ImageButton
android:id="@+id/btn_remote_pause"
android:layout_width="wrap_content"
......
......@@ -36,15 +36,17 @@
android:id="@+id/edt_site_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="@string/title_site_id"
android:inputType="number"
android:visibility="gone"
android:maxLines="1" />
android:maxLines="1"
android:visibility="gone" />
<jp.agentec.abook.abv.ui.common.view.ABVEditText
android:id="@+id/edt_url_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="@string/title_url_path"
android:inputType="textEmailAddress"
android:maxLines="1" />
......@@ -53,6 +55,7 @@
android:id="@+id/edt_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:focusable="true"
android:hint="@string/title_id"
android:inputType="textEmailAddress"
......@@ -75,7 +78,7 @@
android:background="@drawable/btn_login"
android:contentDescription="@string/login"
android:text="@string/login"
android:textColor="@color/text_color"/>
android:textColor="@color/text_color" />
</LinearLayout>
</LinearLayout>
......
......@@ -26,7 +26,7 @@
android:background="@null"
android:src="@drawable/btn_history_back_for_webview" />
<Button
<ImageButton
android:id="@+id/closeBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
......
......@@ -26,7 +26,7 @@
android:background="@drawable/btn_home"
android:contentDescription="@string/end" />
<Button
<ImageButton
android:id="@+id/btn_operation_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
......@@ -180,6 +180,7 @@
android:layout_centerHorizontal="true"
android:layout_marginTop="5dp"
android:background="@drawable/frame"
android:textColor="@android:color/white"
android:padding="8dp"
android:text="@string/dummy_str"
android:textSize="15sp"
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="5dp"
android:textColor="@android:color/darker_gray"
android:textSize="14sp"
android:textStyle="bold"
android:background="@android:color/black"/>
</LinearLayout>
\ No newline at end of file
......@@ -115,6 +115,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="10dp"
android:inputType="text"
android:maxLines="1" />
<LinearLayout
......
......@@ -12,6 +12,7 @@
android:textSize="20sp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:textColor="@android:color/white"
android:background="@drawable/rounded_edittext"
/>
</RelativeLayout>
\ No newline at end of file
......@@ -8,6 +8,7 @@
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollingCache="false" />
android:scrollingCache="false"
android:divider="@android:color/holo_red_dark"/>
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- ソースで文字列連結してアクセスしているためlintに引っかかってしまうので注意 -->
<integer name="marking_size_0">5</integer>
<integer name="marking_size_1">10</integer>
<integer name="marking_size_2">25</integer>
<integer name="marking_size_3">50</integer>
<integer name="marking_size_4">100</integer>
</resources>
\ No newline at end of file
......@@ -3,7 +3,7 @@
<!-- Generated with http://android-holo-colors.com -->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="ABook" parent="android:Theme.Light">
<style name="ABook" parent="Theme.AppCompat.Light">
<!-- <item name="android:editTextStyle">@style/EditTextABook</item> -->
......@@ -30,6 +30,43 @@
<item name="android:textViewStyle">@style/TextViewABook</item>
<item name="android:alertDialogStyle">@style/Theme_ABookAlertDialog</item>
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@android:color/black</item>
<item name="android:statusBarColor">@android:color/black</item>
<!-- add EditText Style -->
<item name="android:editTextColor">@android:color/black</item>
<item name="android:textColorHint">@android:color/darker_gray</item>
<item name="android:editTextBackground">@drawable/bg_edit_text_view</item>
</style>
<style name="Theme.AppCompat.Light.NoActionBar.FullScreen" parent="@style/Theme.AppCompat.Light">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@android:color/black</item>
<!-- add FullScreen Style -->
<item name="android:windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
<!-- add EditText Style -->
<item name="android:editTextColor">@android:color/black</item>
<item name="android:textColorHint">@android:color/darker_gray</item>
<item name="android:editTextBackground">@drawable/bg_edit_text_view</item>
</style>
<!-- add ProgressDialog Style -->
<style name="MyDialogTheme" parent="@style/Theme.AppCompat.Light.Dialog">
<item name="android:colorAccent">@color/colorPrimary</item>
<item name="colorAccent">@color/colorPrimary</item>
<item name="android:background">#00FFFFFF</item>
</style>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
<PreferenceCategory android:title="@string/account" android:key="account_set">
<PreferenceCategory
android:key="account_set"
android:title="@string/account"
android:layout="@layout/custom_preference_category">
<PreferenceScreen
android:key="accountPath"
android:summary="--"
android:title="@string/contractor" >
</PreferenceScreen>
android:title="@string/contractor"></PreferenceScreen>
<PreferenceScreen
android:key="loginid"
android:summary="--"
android:title="@string/logid" >
</PreferenceScreen>
android:title="@string/logid"></PreferenceScreen>
<PreferenceScreen
android:key="lastLogin"
android:summary="--"
android:title="@string/last_login" >
</PreferenceScreen>
android:title="@string/last_login"></PreferenceScreen>
<PreferenceScreen
android:key="passwordChange"
android:summary="@string/password_change_summary"
android:title="@string/pwd_change" >
</PreferenceScreen>
android:title="@string/pwd_change"></PreferenceScreen>
<PreferenceScreen
android:key="logout"
android:summary="@string/logout_summary"
android:title="@string/logout" >
</PreferenceScreen>
android:title="@string/logout"></PreferenceScreen>
</PreferenceCategory>
<PreferenceCategory android:title="@string/log_info" android:key="log_info">
<PreferenceCategory android:title="@string/log_info" android:key="log_info" android:layout="@layout/custom_preference_category">
<CheckBoxPreference
android:defaultValue="true"
android:key="errorSendEnable"
......@@ -40,7 +38,7 @@
android:title="@string/export_title" >
</PreferenceScreen>
</PreferenceCategory>
<PreferenceCategory android:title="@string/app_info" android:key="app_info">
<PreferenceCategory android:title="@string/app_info" android:key="app_info" android:layout="@layout/custom_preference_category">
<PreferenceScreen
android:key="aboutABook"
android:title="@string/about_app" >
......
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cache_files" path="."/>
<external-path name="external_files" path="."/>
</paths>
/*
* Copyright (C) 2012 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.android.vending.billing;
import android.os.Bundle;
/**
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
* This service provides the following features:
* 1. Provides a new API to get details of in-app items published for the app including
* price, type, title and description.
* 2. The purchase flow is synchronous and purchase information is available immediately
* after it completes.
* 3. Purchase information of in-app purchases is maintained within the Google Play system
* till the purchase is consumed.
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
* in-app items are consumable and thereafter can be purchased again.
* 5. An API to get current purchases of the user immediately. This will not contain any
* consumed purchases.
*
* All calls will give a response code with the following possible values
* RESULT_OK = 0 - success
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
* RESULT_ERROR = 6 - Fatal error during the API action
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
*/
interface IInAppBillingService {
/**
* Checks support for the requested billing API version, package and in-app type.
* Minimum API version supported by this interface is 3.
* @param apiVersion the billing version which the app is using
* @param packageName the package name of the calling app
* @param type type of the in-app item being purchased "inapp" for one-time purchases
* and "subs" for subscription.
* @return RESULT_OK(0) on success, corresponding result code on failures
*/
int isBillingSupported(int apiVersion, String packageName, String type);
/**
* Provides details of a list of SKUs
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
* with a list JSON strings containing the productId, price, title and description.
* This API can be called with a maximum of 20 SKUs.
* @param apiVersion billing API version that the Third-party is using
* @param packageName the package name of the calling app
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "DETAILS_LIST" with a StringArrayList containing purchase information
* in JSON format similar to:
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
* "title : "Example Title", "description" : "This is an example description" }'
*/
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
/**
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
* the type, a unique purchase token and an optional developer payload.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type the type of the in-app item ("inapp" for one-time purchases
* and "subs" for subscription).
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
* TODO: change this to app-specific keys.
*/
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
String developerPayload);
/**
* Returns the current SKUs owned by the user of the type and package name specified along with
* purchase information and a signature of the data to be validated.
* This will return all SKUs that have been purchased in V3 and managed items purchased using
* V1 and V2 that have not been consumed.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param type the type of the in-app items being requested
* ("inapp" for one-time purchases and "subs" for subscription).
* @param continuationToken to be set as null for the first call, if the number of owned
* skus are too many, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
* failure as listed above.
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
/**
* Consume the last purchase of the given SKU. This will result in this item being removed
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @return 0 if consumption succeeded. Appropriate error values for failures.
*/
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
}
/* Copyright (c) 2012 Google Inc.
*
* 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 jp.agentec.abook.abv.cl.billing;
/**
* Exception thrown when something went wrong with in-app billing.
* An IabException has an associated IabResult (an error).
* To get the IAB result that caused this exception to be thrown,
* call {@link #getResult()}.
*/
public class IabException extends Exception {
IabResult mResult;
public IabException(IabResult r) {
this(r, null);
}
public IabException(int response, String message) {
this(new IabResult(response, message));
}
public IabException(IabResult r, Exception cause) {
super(r.getMessage(), cause);
mResult = r;
}
public IabException(int response, String message, Exception cause) {
this(new IabResult(response, message), cause);
}
/** Returns the IAB result (error) that this exception signals. */
public IabResult getResult() { return mResult; }
}
\ No newline at end of file
/* Copyright (c) 2012 Google Inc.
*
* 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 jp.agentec.abook.abv.cl.billing;
import java.util.ArrayList;
import java.util.List;
import jp.agentec.abook.abv.bl.common.log.Logger;
import org.json.JSONException;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.TextUtils;
import com.android.vending.billing.IInAppBillingService;
/**
* Provides convenience methods for in-app billing. You can create one instance of this
* class for your application and use it to process in-app billing operations.
* It provides synchronous (blocking) and asynchronous (non-blocking) methods for
* many common in-app billing operations, as well as automatic signature
* verification.
*
* After instantiating, you must perform setup in order to start using the object.
* To perform setup, call the {@link #startSetup} method and provide a listener;
* that listener will be notified when setup is complete, after which (and not before)
* you may call other methods.
*
* After setup is complete, you will typically want to request an inventory of owned
* items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
* and related methods.
*
* When you are done with this object, don't forget to call {@link #dispose}
* to ensure proper cleanup. This object holds a binding to the in-app billing
* service, which will leak unless you dispose of it correctly. If you created
* the object on an Activity's onCreate method, then the recommended
* place to dispose of it is the Activity's onDestroy method.
*
* A note about threading: When using this object from a background thread, you may
* call the blocking versions of methods; when using from a UI thread, call
* only the asynchronous versions and handle the results via callbacks.
* Also, notice that you can only call one asynchronous operation at a time;
* attempting to start a second asynchronous operation while the first one
* has not yet completed will result in an exception being thrown.
*
* @author Bruno Oliveira (Google)
*
*/
public class IabHelper {
// Is debug logging enabled?
boolean mDebugLog = false;
String mDebugTag = "IabHelper";
// Is setup done?
boolean mSetupDone = false;
// Has this object been disposed of? (If so, we should ignore callbacks, etc)
boolean mDisposed = false;
// Are subscriptions supported?
boolean mSubscriptionsSupported = false;
// Is an asynchronous operation in progress?
// (only one at a time can be in progress)
boolean mAsyncInProgress = false;
// (for logging/debugging)
// if mAsyncInProgress == true, what asynchronous operation is in progress?
String mAsyncOperation = "";
// Context we were passed during initialization
Context mContext;
// Connection to the service
IInAppBillingService mService;
ServiceConnection mServiceConn;
// The request code used to launch purchase flow
int mRequestCode;
// The item type of the current purchase flow
String mPurchasingItemType;
// Public key for verifying signature, in base64 encoding
String mSignatureBase64 = null;
// Billing response codes
public static final int BILLING_RESPONSE_RESULT_OK = 0;
public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
// IAB Helper error codes
public static final int IABHELPER_ERROR_BASE = -1000;
public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
public static final int IABHELPER_BAD_RESPONSE = -1002;
public static final int IABHELPER_VERIFICATION_FAILED = -1003;
public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
public static final int IABHELPER_USER_CANCELLED = -1005;
public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
public static final int IABHELPER_MISSING_TOKEN = -1007;
public static final int IABHELPER_UNKNOWN_ERROR = -1008;
public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
// Keys for the responses from InAppBillingService
public static final String RESPONSE_CODE = "RESPONSE_CODE";
public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
// Item types
public static final String ITEM_TYPE_INAPP = "inapp";
public static final String ITEM_TYPE_SUBS = "subs";
// some fields on the getSkuDetails response bundle
public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
/**
* Creates an instance. After creation, it will not yet be ready to use. You must perform
* setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
* block and is safe to call from a UI thread.
*
* @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
* @param base64PublicKey Your application's public key, encoded in base64.
* This is used for verification of purchase signatures. You can find your app's base64-encoded
* public key in your application's page on Google Play Developer Console. Note that this
* is NOT your "developer public key".
*/
public IabHelper(Context ctx, String base64PublicKey) {
mContext = ctx.getApplicationContext();
mSignatureBase64 = base64PublicKey;
logDebug("IAB helper created.");
}
/**
* Enables or disable debug logging through LogCat.
*/
public void enableDebugLogging(boolean enable, String tag) {
checkNotDisposed();
mDebugLog = enable;
mDebugTag = tag;
}
public void enableDebugLogging(boolean enable) {
checkNotDisposed();
mDebugLog = enable;
}
/**
* Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
* when the setup process is complete.
*/
public interface OnIabSetupFinishedListener {
/**
* Called to notify that setup is complete.
*
* @param result The result of the setup process.
*/
public void onIabSetupFinished(IabResult result);
}
/**
* Starts the setup process. This will start up the setup process asynchronously.
* You will be notified through the listener when the setup process is complete.
* This method is safe to call from a UI thread.
*
* @param listener The listener to notify when the setup process is complete.
*/
public void startSetup(final OnIabSetupFinishedListener listener) {
// If already set up, can't do it again.
checkNotDisposed();
if (mSetupDone) {
throw new IllegalStateException("IAB helper is already set up.");
}
// Connection to IAB service
logDebug("Starting in-app billing setup.");
mServiceConn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
logDebug("Billing service disconnected.");
mService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (mDisposed) {
return;
}
logDebug("Billing service connected.");
mService = IInAppBillingService.Stub.asInterface(service);
String packageName = mContext.getPackageName();
try {
logDebug("Checking for in-app billing 3 support.");
// check for in-app billing v3 support
int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
if (response != BILLING_RESPONSE_RESULT_OK) {
if (listener != null) {
listener.onIabSetupFinished(new IabResult(response,
"Error checking for billing v3 support."));
}
// if in-app purchases aren't supported, neither are subscriptions.
mSubscriptionsSupported = false;
return;
}
logDebug("In-app billing version 3 supported for " + packageName);
// check for v3 subscriptions support
response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
if (response == BILLING_RESPONSE_RESULT_OK) {
logDebug("Subscriptions AVAILABLE.");
mSubscriptionsSupported = true;
}
else {
logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
}
mSetupDone = true;
}
catch (RemoteException e) {
if (listener != null) {
listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
"RemoteException while setting up in-app billing."));
}
e.printStackTrace();
return;
}
if (listener != null) {
listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
}
}
};
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
serviceIntent.setPackage("com.android.vending");
if (mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
// no service available to handle that Intent
if (listener != null) {
listener.onIabSetupFinished(
new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
"Billing service unavailable on device."));
}
} else {
// service available to handle that Intent
mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
}
}
/**
* Dispose of object, releasing resources. It's very important to call this
* method when you are done with this object. It will release any resources
* used by it such as service connections. Naturally, once the object is
* disposed of, it can't be used again.
*/
public void dispose() {
logDebug("Disposing.");
mSetupDone = false;
if (mServiceConn != null) {
logDebug("Unbinding from service.");
if (mContext != null) {
mContext.unbindService(mServiceConn);
}
}
mDisposed = true;
mContext = null;
mServiceConn = null;
mService = null;
mPurchaseListener = null;
}
private void checkNotDisposed() {
if (mDisposed) {
throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
}
}
/** Returns whether subscriptions are supported. */
public boolean subscriptionsSupported() {
checkNotDisposed();
return mSubscriptionsSupported;
}
/**
* Callback that notifies when a purchase is finished.
*/
public interface OnIabPurchaseFinishedListener {
/**
* Called to notify that an in-app purchase finished. If the purchase was successful,
* then the sku parameter specifies which item was purchased. If the purchase failed,
* the sku and extraData parameters may or may not be null, depending on how far the purchase
* process went.
*
* @param result The result of the purchase.
* @param info The purchase information (null if purchase failed)
*/
public void onIabPurchaseFinished(IabResult result, Purchase info);
}
// The listener registered on launchPurchaseFlow, which we have to call back when
// the purchase finishes
OnIabPurchaseFinishedListener mPurchaseListener;
public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
launchPurchaseFlow(act, sku, requestCode, listener, "");
}
public void launchPurchaseFlow(Activity act, String sku, int requestCode,
OnIabPurchaseFinishedListener listener, String extraData) {
launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
}
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
OnIabPurchaseFinishedListener listener) {
launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
}
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
OnIabPurchaseFinishedListener listener, String extraData) {
launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
}
/**
* Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
* which will involve bringing up the Google Play screen. The calling activity will be paused while
* the user interacts with Google Play, and the result will be delivered via the activity's
* {@link android.app.Activity#onActivityResult} method, at which point you must call
* this object's {@link #handleActivityResult} method to continue the purchase flow. This method
* MUST be called from the UI thread of the Activity.
*
* @param act The calling activity.
* @param sku The sku of the item to purchase.
* @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
* @param requestCode A request code (to differentiate from other responses --
* as in {@link android.app.Activity#startActivityForResult}).
* @param listener The listener to notify when the purchase process finishes
* @param extraData Extra data (developer payload), which will be returned with the purchase data
* when the purchase completes. This extra data will be permanently bound to that purchase
* and will always be returned when the purchase is queried.
*/
public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
OnIabPurchaseFinishedListener listener, String extraData) {
checkNotDisposed();
checkSetupDone("launchPurchaseFlow");
flagStartAsync("launchPurchaseFlow");
IabResult result;
if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
"Subscriptions are not available.");
flagEndAsync();
if (listener != null) {
listener.onIabPurchaseFinished(r, null);
}
return;
}
try {
logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
int response = getResponseCodeFromBundle(buyIntentBundle);
if (response != BILLING_RESPONSE_RESULT_OK) {
logError("Unable to buy item, Error response: " + getResponseDesc(response));
flagEndAsync();
result = new IabResult(response, "Unable to buy item");
if (listener != null) {
listener.onIabPurchaseFinished(result, null);
}
return;
}
PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
mRequestCode = requestCode;
mPurchaseListener = listener;
mPurchasingItemType = itemType;
if (pendingIntent != null) {
act.startIntentSenderForResult(pendingIntent.getIntentSender(),
requestCode, new Intent(),
Integer.valueOf(0), Integer.valueOf(0),
Integer.valueOf(0));
}
}
catch (SendIntentException e) {
logError("SendIntentException while launching purchase flow for sku " + sku);
e.printStackTrace();
flagEndAsync();
result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
if (listener != null) {
listener.onIabPurchaseFinished(result, null);
}
}
catch (RemoteException e) {
logError("RemoteException while launching purchase flow for sku " + sku);
e.printStackTrace();
flagEndAsync();
result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
if (listener != null) {
listener.onIabPurchaseFinished(result, null);
}
}
}
/**
* Handles an activity result that's part of the purchase flow in in-app billing. If you
* are calling {@link #launchPurchaseFlow}, then you must call this method from your
* Activity's {@link android.app.Activity@onActivityResult} method. This method
* MUST be called from the UI thread of the Activity.
*
* @param requestCode The requestCode as you received it.
* @param resultCode The resultCode as you received it.
* @param data The data (Intent) as you received it.
* @return Returns true if the result was related to a purchase flow and was handled;
* false if the result was not related to a purchase, in which case you should
* handle it normally.
*/
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
IabResult result;
if (requestCode != mRequestCode) {
return false;
}
checkNotDisposed();
checkSetupDone("handleActivityResult");
// end of async purchase operation that started on launchPurchaseFlow
flagEndAsync();
if (data == null) {
logError("Null data in IAB activity result.");
result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
if (mPurchaseListener != null) {
mPurchaseListener.onIabPurchaseFinished(result, null);
}
return true;
}
int responseCode = getResponseCodeFromIntent(data);
String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
logDebug("Successful resultcode from purchase activity.");
logDebug("Purchase data: " + purchaseData);
logDebug("Data signature: " + dataSignature);
logDebug("Extras: " + data.getExtras());
logDebug("Expected item type: " + mPurchasingItemType);
if (purchaseData == null || dataSignature == null) {
logError("BUG: either purchaseData or dataSignature is null.");
logDebug("Extras: " + data.getExtras().toString());
result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
if (mPurchaseListener != null) {
mPurchaseListener.onIabPurchaseFinished(result, null);
}
return true;
}
Purchase purchase;
try {
purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
String sku = purchase.getSku();
// Verify signature
if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
logError("Purchase signature verification FAILED for sku " + sku);
result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
if (mPurchaseListener != null) {
mPurchaseListener.onIabPurchaseFinished(result, purchase);
}
return true;
}
logDebug("Purchase signature successfully verified.");
}
catch (JSONException e) {
logError("Failed to parse purchase data.");
e.printStackTrace();
result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
if (mPurchaseListener != null) {
mPurchaseListener.onIabPurchaseFinished(result, null);
}
return true;
}
if (mPurchaseListener != null) {
mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
}
}
else if (resultCode == Activity.RESULT_OK) {
// result code was OK, but in-app billing response was not OK.
logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
if (mPurchaseListener != null) {
result = new IabResult(responseCode, "Problem purchashing item.");
mPurchaseListener.onIabPurchaseFinished(result, null);
}
}
else if (resultCode == Activity.RESULT_CANCELED) {
logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
if (mPurchaseListener != null) {
mPurchaseListener.onIabPurchaseFinished(result, null);
}
}
else {
logError("Purchase failed. Result code: " + Integer.toString(resultCode)
+ ". Response: " + getResponseDesc(responseCode));
result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
if (mPurchaseListener != null) {
mPurchaseListener.onIabPurchaseFinished(result, null);
}
}
return true;
}
public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException {
return queryInventory(querySkuDetails, moreSkus, null);
}
/**
* Queries the inventory. This will query all owned items from the server, as well as
* information on additional skus, if specified. This method may block or take long to execute.
* Do not call from a UI thread. For that, use the non-blocking version.
*
* @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
* as purchase information.
* @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
* Ignored if null or if querySkuDetails is false.
* @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
* Ignored if null or if querySkuDetails is false.
* @throws IabException if a problem occurs while refreshing the inventory.
*/
public Inventory queryInventory(boolean querySkuDetails, List<String> moreItemSkus,
List<String> moreSubsSkus) throws IabException {
checkNotDisposed();
checkSetupDone("queryInventory");
try {
Inventory inv = new Inventory();
int r = queryPurchases(inv, ITEM_TYPE_INAPP);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying owned items).");
}
if (querySkuDetails) {
r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying prices of items).");
}
}
// if subscriptions are supported, then also query for subscriptions
if (mSubscriptionsSupported) {
r = queryPurchases(inv, ITEM_TYPE_SUBS);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
}
if (querySkuDetails) {
r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
}
}
}
return inv;
}
catch (RemoteException e) {
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
}
catch (JSONException e) {
throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
}
}
/**
* Listener that notifies when an inventory query operation completes.
*/
public interface QueryInventoryFinishedListener {
/**
* Called to notify that an inventory query operation completed.
*
* @param result The result of the operation.
* @param inv The inventory.
*/
public void onQueryInventoryFinished(IabResult result, Inventory inv);
}
/**
* Asynchronous wrapper for inventory query. This will perform an inventory
* query as described in {@link #queryInventory}, but will do so asynchronously
* and call back the specified listener upon completion. This method is safe to
* call from a UI thread.
*
* @param querySkuDetails as in {@link #queryInventory}
* @param moreSkus as in {@link #queryInventory}
* @param listener The listener to notify when the refresh operation completes.
*/
public void queryInventoryAsync(final boolean querySkuDetails,
final List<String> moreSkus,
final QueryInventoryFinishedListener listener) {
final Handler handler = new Handler();
checkNotDisposed();
checkSetupDone("queryInventory");
flagStartAsync("refresh inventory");
(new Thread(new Runnable() {
@Override
public void run() {
IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
Inventory inv = null;
try {
inv = queryInventory(querySkuDetails, moreSkus);
}
catch (IabException ex) {
result = ex.getResult();
}
finally {
flagEndAsync();
}
final IabResult result_f = result;
final Inventory inv_f = inv;
if (!mDisposed && listener != null) {
handler.post(new Runnable() {
@Override
public void run() {
listener.onQueryInventoryFinished(result_f, inv_f);
}
});
}
}
})).start();
}
// public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
// queryInventoryAsync(true, null, listener);
// }
//
// public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
// queryInventoryAsync(querySkuDetails, null, listener);
// }
/**
* Consumes a given in-app product. Consuming can only be done on an item
* that's owned, and as a result of consumption, the user will no longer own it.
* This method may block or take long to return. Do not call from the UI thread.
* For that, see {@link #consumeAsync}.
*
* @param itemInfo The PurchaseInfo that represents the item to consume.
* @throws IabException if there is a problem during consumption.
*/
void consume(Purchase itemInfo) throws IabException {
checkNotDisposed();
checkSetupDone("consume");
if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
throw new IabException(IABHELPER_INVALID_CONSUMPTION,
"Items of type '" + itemInfo.mItemType + "' can't be consumed.");
}
try {
String token = itemInfo.getToken();
String sku = itemInfo.getSku();
if (token == null || token.equals("")) {
logError("Can't consume "+ sku + ". No token.");
throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+ sku + " " + itemInfo);
}
logDebug("Consuming sku: " + sku + ", token: " + token);
int response = mService.consumePurchase(3, mContext.getPackageName(), token);
if (response == BILLING_RESPONSE_RESULT_OK) {
logDebug("Successfully consumed sku: " + sku);
}
else {
logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
throw new IabException(response, "Error consuming sku " + sku);
}
}
catch (RemoteException e) {
throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
}
}
/**
* Callback that notifies when a consumption operation finishes.
*/
public interface OnConsumeFinishedListener {
/**
* Called to notify that a consumption has finished.
*
* @param purchase The purchase that was (or was to be) consumed.
* @param result The result of the consumption operation.
*/
public void onConsumeFinished(Purchase purchase, IabResult result);
}
/**
* Callback that notifies when a multi-item consumption operation finishes.
*/
public interface OnConsumeMultiFinishedListener {
/**
* Called to notify that a consumption of multiple items has finished.
*
* @param purchases The purchases that were (or were to be) consumed.
* @param results The results of each consumption operation, corresponding to each
* sku.
*/
public void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results);
}
/**
* Asynchronous wrapper to item consumption. Works like {@link #consume}, but
* performs the consumption in the background and notifies completion through
* the provided listener. This method is safe to call from a UI thread.
*
* @param purchase The purchase to be consumed.
* @param listener The listener to notify when the consumption operation finishes.
*/
public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
checkNotDisposed();
checkSetupDone("consume");
List<Purchase> purchases = new ArrayList<>();
purchases.add(purchase);
consumeAsyncInternal(purchases, listener, null);
}
/**
* Same as {@link #consumeAsync}, but for multiple items at once.
* @param purchases The list of PurchaseInfo objects representing the purchases to consume.
* @param listener The listener to notify when the consumption operation finishes.
*/
public void consumeAsync(List<Purchase> purchases, OnConsumeMultiFinishedListener listener) {
checkNotDisposed();
checkSetupDone("consume");
consumeAsyncInternal(purchases, null, listener);
}
/**
* Returns a human-readable description for the given response code.
*
* @param code The response code
* @return A human-readable string explaining the result code.
* It also includes the result code numerically.
*/
public static String getResponseDesc(int code) {
String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
"3:Billing Unavailable/4:Item unavailable/" +
"5:Developer Error/6:Error/7:Item Already Owned/" +
"8:Item not owned").split("/");
String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
"-1002:Bad response received/" +
"-1003:Purchase signature verification failed/" +
"-1004:Send intent failed/" +
"-1005:User cancelled/" +
"-1006:Unknown purchase response/" +
"-1007:Missing token/" +
"-1008:Unknown error/" +
"-1009:Subscriptions not available/" +
"-1010:Invalid consumption attempt").split("/");
if (code <= IABHELPER_ERROR_BASE) {
int index = IABHELPER_ERROR_BASE - code;
if (index >= 0 && index < iabhelper_msgs.length) {
return iabhelper_msgs[index];
} else {
return String.valueOf(code) + ":Unknown IAB Helper Error";
}
}
else if (code < 0 || code >= iab_msgs.length) {
return String.valueOf(code) + ":Unknown";
} else {
return iab_msgs[code];
}
}
// Checks that setup was done; if not, throws an exception.
void checkSetupDone(String operation) {
if (!mSetupDone) {
logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
}
}
// Workaround to bug where sometimes response codes come as Long instead of Integer
int getResponseCodeFromBundle(Bundle b) {
Object o = b.get(RESPONSE_CODE);
if (o == null) {
logDebug("Bundle with null response code, assuming OK (known issue)");
return BILLING_RESPONSE_RESULT_OK;
}
else if (o instanceof Integer) {
return ((Integer) o).intValue();
} else if (o instanceof Long) {
return (int) ((Long) o).longValue();
} else {
logError("Unexpected type for bundle response code.");
logError(o.getClass().getName());
throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
}
}
// Workaround to bug where sometimes response codes come as Long instead of Integer
int getResponseCodeFromIntent(Intent i) {
Object o = i.getExtras().get(RESPONSE_CODE);
if (o == null) {
logError("Intent with no response code, assuming OK (known issue)");
return BILLING_RESPONSE_RESULT_OK;
}
else if (o instanceof Integer) {
return ((Integer) o).intValue();
} else if (o instanceof Long) {
return (int) ((Long) o).longValue();
} else {
logError("Unexpected type for intent response code.");
logError(o.getClass().getName());
throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
}
}
void flagStartAsync(String operation) { // FIXME: mAsyncInProgressが正しくリセットされていない
// if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
// operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
mAsyncOperation = operation;
mAsyncInProgress = true;
logDebug("Starting async operation: " + operation);
}
void flagEndAsync() {
logDebug("Ending async operation: " + mAsyncOperation);
mAsyncOperation = "";
mAsyncInProgress = false;
}
int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
// Query purchases
logDebug("Querying owned items, item type: " + itemType);
logDebug("Package name: " + mContext.getPackageName());
boolean verificationFailed = false;
String continueToken = null;
do {
logDebug("Calling getPurchases with continuation token: " + continueToken);
Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
itemType, continueToken);
int response = getResponseCodeFromBundle(ownedItems);
logDebug("Owned items response: " + String.valueOf(response));
if (response != BILLING_RESPONSE_RESULT_OK) {
logDebug("getPurchases() failed: " + getResponseDesc(response));
return response;
}
if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
|| !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
|| !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
logError("Bundle returned from getPurchases() doesn't contain required fields.");
return IABHELPER_BAD_RESPONSE;
}
ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
RESPONSE_INAPP_ITEM_LIST);
ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
RESPONSE_INAPP_PURCHASE_DATA_LIST);
ArrayList<String> signatureList = ownedItems.getStringArrayList(
RESPONSE_INAPP_SIGNATURE_LIST);
if (purchaseDataList != null && signatureList != null) {
for (int i = 0; i < purchaseDataList.size(); ++i) {
String purchaseData = purchaseDataList.get(i);
String signature = signatureList.get(i);
String sku = null;
if( ownedSkus != null) {
sku = ownedSkus.get(i);
}
if (sku != null && sku.startsWith("android.test")) { // なぜかsignatureがemptyになっている
continue;
}
Logger.d(getClass().getName(), "sku=%s, mSignatureBase64=%s, purchaseData=%s, signature=%s", sku, mSignatureBase64, purchaseData, signature);
if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
logDebug("Sku is owned: " + sku);
Purchase purchase = new Purchase(itemType, purchaseData, signature);
logDebug("Purchase data: " + purchaseData);
if (TextUtils.isEmpty(purchase.getToken())) {
logWarn("BUG: empty/null token!");
}
// Record ownership and token
inv.addPurchase(purchase);
} else {
logWarn("Purchase signature verification **FAILED**. Not adding item.");
logDebug(" Purchase data: " + purchaseData);
logDebug(" Signature: " + signature);
verificationFailed = true;
}
}
}
continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
logDebug("Continuation token: " + continueToken);
} while (!TextUtils.isEmpty(continueToken));
return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
}
int querySkuDetails(String itemType, Inventory inv, List<String> moreSkus)
throws RemoteException, JSONException {
logDebug("Querying SKU details.");
ArrayList<String> skuList = new ArrayList<>();
// skuList.addAll(inv.getAllOwnedSkus(itemType)); // これをすると購入済みを検索対象に加えてしまって20件制限を超えてしまう
if (moreSkus != null) {
for (String sku : moreSkus) {
if (!skuList.contains(sku)) {
skuList.add(sku);
}
}
}
if (skuList.size() == 0) {
logDebug("queryPrices: nothing to do because there are no SKUs.");
return BILLING_RESPONSE_RESULT_OK;
}
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
itemType, querySkus);
if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
int response = getResponseCodeFromBundle(skuDetails);
if (response == BILLING_RESPONSE_RESULT_OK) {
logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
return IABHELPER_BAD_RESPONSE;
} else {
logDebug("getSkuDetails() failed: " + getResponseDesc(response));
return response;
}
}
ArrayList<String> responseList = skuDetails.getStringArrayList(
RESPONSE_GET_SKU_DETAILS_LIST);
if (responseList != null) {
for (String thisResponse : responseList) {
SkuDetails d = new SkuDetails(itemType, thisResponse);
logDebug("Got sku details: " + d);
inv.addSkuDetails(d);
}
}
return BILLING_RESPONSE_RESULT_OK;
}
void consumeAsyncInternal(final List<Purchase> purchases,
final OnConsumeFinishedListener singleListener,
final OnConsumeMultiFinishedListener multiListener) {
final Handler handler = new Handler();
flagStartAsync("consume");
(new Thread(new Runnable() {
@Override
public void run() {
final List<IabResult> results = new ArrayList<>();
for (Purchase purchase : purchases) {
try {
consume(purchase);
results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
}
catch (IabException ex) {
results.add(ex.getResult());
}
}
flagEndAsync();
if (!mDisposed && singleListener != null) {
handler.post(new Runnable() {
@Override
public void run() {
singleListener.onConsumeFinished(purchases.get(0), results.get(0));
}
});
}
if (!mDisposed && multiListener != null) {
handler.post(new Runnable() {
@Override
public void run() {
multiListener.onConsumeMultiFinished(purchases, results);
}
});
}
}
})).start();
}
void logDebug(String msg) {
if (mDebugLog) {
Logger.d(mDebugTag, msg);
}
}
void logError(String msg) {
Logger.e(mDebugTag, "In-app billing error: " + msg);
}
void logWarn(String msg) {
Logger.w(mDebugTag, "In-app billing warning: " + msg);
}
}
/* Copyright (c) 2012 Google Inc.
*
* 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 jp.agentec.abook.abv.cl.billing;
/**
* Represents the result of an in-app billing operation.
* A result is composed of a response code (an integer) and possibly a
* message (String). You can get those by calling
* {@link #getResponse} and {@link #getMessage()}, respectively. You
* can also inquire whether a result is a success or a failure by
* calling {@link #isSuccess()} and {@link #isFailure()}.
*/
public class IabResult {
int mResponse;
String mMessage;
public IabResult(int response, String message) {
mResponse = response;
if (message == null || message.trim().length() == 0) {
mMessage = IabHelper.getResponseDesc(response);
}
else {
mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
}
}
public int getResponse() { return mResponse; }
public String getMessage() { return mMessage; }
public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
public boolean isFailure() { return !isSuccess(); }
public String toString() { return "IabResult: " + getMessage(); }
}
/* Copyright (c) 2012 Google Inc.
*
* 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 jp.agentec.abook.abv.cl.billing;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents a block of information about in-app items.
* An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
*/
public class Inventory {
Map<String,SkuDetails> mSkuMap = new HashMap<>();
Map<String,Purchase> mPurchaseMap = new HashMap<>();
Inventory() { }
/** Returns the listing details for an in-app product. */
public SkuDetails getSkuDetails(String sku) {
return mSkuMap.get(sku);
}
/** Returns purchase information for a given product, or null if there is no purchase. */
public Purchase getPurchase(String sku) {
return mPurchaseMap.get(sku);
}
/** Returns whether or not there exists a purchase of the given product. */
public boolean hasPurchase(String sku) {
return mPurchaseMap.containsKey(sku);
}
/** Return whether or not details about the given product are available. */
public boolean hasDetails(String sku) {
return mSkuMap.containsKey(sku);
}
/**
* Erase a purchase (locally) from the inventory, given its product ID. This just
* modifies the Inventory object locally and has no effect on the server! This is
* useful when you have an existing Inventory object which you know to be up to date,
* and you have just consumed an item successfully, which means that erasing its
* purchase data from the Inventory you already have is quicker than querying for
* a new Inventory.
*/
public void erasePurchase(String sku) {
if (mPurchaseMap.containsKey(sku)) {
mPurchaseMap.remove(sku);
}
}
/** Returns a list of all owned product IDs. */
List<String> getAllOwnedSkus() {
return new ArrayList<>(mPurchaseMap.keySet());
}
/** Returns a list of all owned product IDs of a given type */
List<String> getAllOwnedSkus(String itemType) {
List<String> result = new ArrayList<>();
for (Purchase p : mPurchaseMap.values()) {
if (p.getItemType().equals(itemType)) {
result.add(p.getSku());
}
}
return result;
}
/** Returns a list of all purchases. */
List<Purchase> getAllPurchases() {
return new ArrayList<>(mPurchaseMap.values());
}
void addSkuDetails(SkuDetails d) {
mSkuMap.put(d.getSku(), d);
}
void addPurchase(Purchase p) {
mPurchaseMap.put(p.getSku(), p);
}
}
/* Copyright (c) 2012 Google Inc.
*
* 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 jp.agentec.abook.abv.cl.billing;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Represents an in-app billing purchase.
*/
public class Purchase {
String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
String mOrderId;
String mPackageName;
String mSku;
long mPurchaseTime;
int mPurchaseState;
String mDeveloperPayload;
String mToken;
String mOriginalJson;
String mSignature;
public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
mItemType = itemType;
mOriginalJson = jsonPurchaseInfo;
JSONObject o = new JSONObject(mOriginalJson);
mOrderId = o.optString("orderId");
mPackageName = o.optString("packageName");
mSku = o.optString("productId");
mPurchaseTime = o.optLong("purchaseTime");
mPurchaseState = o.optInt("purchaseState");
mDeveloperPayload = o.optString("developerPayload");
mToken = o.optString("token", o.optString("purchaseToken"));
mSignature = signature;
}
public String getItemType() { return mItemType; }
public String getOrderId() { return mOrderId; }
public String getPackageName() { return mPackageName; }
public String getSku() { return mSku; }
public long getPurchaseTime() { return mPurchaseTime; }
public int getPurchaseState() { return mPurchaseState; }
public String getDeveloperPayload() { return mDeveloperPayload; }
public String getToken() { return mToken; }
public String getOriginalJson() { return mOriginalJson; }
public String getSignature() { return mSignature; }
@Override
public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
}
/* Copyright (c) 2012 Google Inc.
*
* 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 jp.agentec.abook.abv.cl.billing;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import android.text.TextUtils;
import android.util.Log;
/**
* Security-related methods. For a secure implementation, all of this code
* should be implemented on a server that communicates with the
* application on the device. For the sake of simplicity and clarity of this
* example, this code is included here and is executed on the device. If you
* must verify the purchases on the phone, you should obfuscate this code to
* make it harder for an attacker to replace the code with stubs that treat all
* purchases as verified.
*/
public class Security {
private static final String TAG = "IABUtil/Security";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* Verifies that the data was signed with the given signature, and returns
* the verified purchase. The data is in JSON format and signed
* with a private key. The data also contains the PurchaseState
* and product ID of the purchase.
* @param base64PublicKey the base64-encoded public key to use for verifying.
* @param signedData the signed JSON string (signed, not encrypted)
* @param signature the signature for the data, signed with the private key
*/
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
TextUtils.isEmpty(signature)) {
Log.e(TAG, "Purchase verification failed: missing data.");
return false;
}
PublicKey key = Security.generatePublicKey(base64PublicKey);
return Security.verify(key, signedData, signature);
}
/**
* Generates a PublicKey instance from a string containing the
* Base64-encoded public key.
*
* @param encodedPublicKey Base64-encoded public key
* @throws IllegalArgumentException if encodedPublicKey is invalid
*/
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
Log.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
throw new IllegalArgumentException(e);
}
}
/**
* Verifies that the signature from the server matches the computed
* signature on the data. Returns true if the data is correctly signed.
*
* @param publicKey public key associated with the developer account
* @param signedData signed data from server
* @param signature server signature
* @return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
Signature sig;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature))) {
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
}
return false;
}
}
/* Copyright (c) 2012 Google Inc.
*
* 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 jp.agentec.abook.abv.cl.billing;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Represents an in-app product's listing details.
*/
public class SkuDetails {
String mItemType;
String mSku;
String mType;
String mPrice;
String mTitle;
String mDescription;
String mJson;
public SkuDetails(String jsonSkuDetails) throws JSONException {
this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
}
public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
mItemType = itemType;
mJson = jsonSkuDetails;
JSONObject o = new JSONObject(mJson);
mSku = o.optString("productId");
mType = o.optString("type");
mPrice = o.optString("price");
mTitle = o.optString("title");
mDescription = o.optString("description");
}
public String getSku() { return mSku; }
public String getType() { return mType; }
public String getPrice() { return mPrice; }
public String getTitle() { return mTitle; }
public String getDescription() { return mDescription; }
@Override
public String toString() {
return "SkuDetails:" + mJson;
}
}
......@@ -451,5 +451,39 @@ public class BitmapUtil {
return Base64.encodeToString(outputStream.toByteArray(), Base64.DEFAULT);
}
/**
* アスペクト比を維持して、横に引き延ばしたBitmapを作成する
* @param filePath ファイルパス
* @param dispWidth 画面の幅
* @param dispHeight 画面の高さ
* @return リサイズしたBitmap
*/
public static Bitmap getResizedBitmap(String filePath, int dispWidth, int dispHeight) {
// Bitmap情報取得
BitmapFactory.Options options = BitmapUtil.getBitmapDimensions(filePath);
int bitmapWidth = options.outWidth;
int bitmpaHeight = options.outHeight;
// 変更するサイズを計算
int targetW;
int targetH;
if (bitmapWidth > bitmpaHeight) {
targetW = dispWidth;
targetH = (int) ((float) bitmpaHeight * (float) dispWidth / (float) bitmapWidth);
if (targetH > dispHeight) {
targetH = dispHeight;
targetW = (int) ((float) bitmapWidth * (float) dispHeight / (float) bitmpaHeight);
}
} else {
targetH = dispHeight;
targetW = (int) ((float) bitmapWidth * (float) dispHeight / (float) bitmpaHeight);
if (targetW > dispWidth) {
targetW = dispWidth;
targetH = (int) ((float) bitmpaHeight * (float) dispWidth / (float) bitmapWidth);
}
}
return BitmapUtil.getResizedBitmap(filePath, targetW, targetH, Config.RGB_565, true);
}
}
package jp.agentec.abook.abv.launcher.android;
import android.app.DownloadManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.support.v4.content.FileProvider;
import java.io.File;
import jp.agentec.abook.abv.bl.acms.type.AcmsApis;
import jp.agentec.abook.abv.bl.common.ABVEnvironment;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.bl.data.ABVDataCache;
import jp.agentec.adf.util.DateTimeFormat;
import jp.agentec.adf.util.DateTimeUtil;
public class BackgroundDownloadService extends Service {
private final static String LOG_TAG = BackgroundDownloadService.class.getSimpleName();
private DownloadManager mDownloadManager;
private long mDownloadedFileID;
private String notificationChannelId;
private String notificationChannelIdLow;
private NotificationManager mNotificationManager;
@Override
public void onCreate() {
super.onCreate();
mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
notificationChannelId = getApplicationContext().getPackageName();
notificationChannelIdLow += ".Low";
if (mNotificationManager == null) {
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
NotificationChannel notificationChannel = new NotificationChannel(notificationChannelId, getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT);
NotificationChannel notificationChannelLow = new NotificationChannel(notificationChannelIdLow, getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW);
if (mNotificationManager != null) {
mNotificationManager.createNotificationChannel(notificationChannel);
mNotificationManager.createNotificationChannel(notificationChannelLow);
}
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
return START_STICKY;
}
BroadcastReceiver onDownloadComplete = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
unregisterReceiver(this);
// Prevents the occasional unintentional call.
if (mDownloadedFileID == -1) {
return;
}
try {
File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), ABVEnvironment.APK_FILE_NAME);
if (file.exists()) {
Uri apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file);
Intent updateIntent = new Intent(Intent.ACTION_VIEW);
updateIntent.setDataAndType(apkUri, "application/vnd.android.package-archive");
updateIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
updateIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Android10(Q)
if (Build.VERSION.SDK_INT >= 29) {
sendNotify(updateIntent, getString(R.string.app_update), false);
Logger.d(LOG_TAG, "sendNotify(updateIntent...).");
} else {
stopForeground(true);
startActivity(updateIntent);
Logger.d(LOG_TAG, "startActivity(fileIntent).");
}
} else {
sendNotify(intent, getString(R.string.DOWNLOAD_ERROR), false);
Logger.w(LOG_TAG, "sendNotify(intent...).");
}
} catch (Exception ex) {
sendNotify(intent, getString(R.string.DOWNLOAD_ERROR), false);
Logger.e(LOG_TAG, "startActivity(fileIntent).", ex);
} finally {
// Sets up the prevention of an unintentional call.
mDownloadedFileID = -1;
}
}
};
try {
ABVEnvironment abvEnvironment = ABVEnvironment.getInstance();
ABVDataCache dataCache = ABVDataCache.getInstance();
String currentDate = DateTimeUtil.toString(DateTimeUtil.getCurrentTimestamp(), DateTimeFormat.yyyyMMddHHmmss000_none);
String downloadUrl = AcmsApis.getDownloadApplicationFileUrl(abvEnvironment.acmsAddress, dataCache.getUrlPath(), dataCache.getMemberInfo().sid, currentDate);
Logger.d(LOG_TAG, "downloadUrl=%s", downloadUrl);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downloadUrl));
request.setDescription("ABook Plus New Version File");
//LANケーブル接続のタイプ(ETHERNET TYPE)がないため、セットしない
//request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), ABVEnvironment.APK_FILE_NAME);
Logger.d(LOG_TAG, "download local file=%s", file.getAbsolutePath());
File[] childs = file.getParentFile().listFiles();
if (childs != null && childs.length > 0 && childs[0].exists()) {
childs[0].delete();
}
request.setDestinationUri(Uri.fromFile(file));
// Registers function to listen to the completion of the download.
registerReceiver(onDownloadComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
// Download 開始
Logger.d(LOG_TAG, "mDownloadManager.enqueue(request).");
mDownloadedFileID = mDownloadManager.enqueue(request);
//5秒以内にstartForegroundを呼ばないとクラッシュ
int uniqueId = (int) System.currentTimeMillis();
Notification notification = getNotification(uniqueId, intent, getString(R.string.download_start), true);
startForeground(uniqueId, notification);
} catch (Exception ex) {
Logger.e(LOG_TAG, "mDownloadManager.enqueue(request).", ex);
}
return super.onStartCommand(intent, flags, startId);
}
private void sendNotify(Intent intent, String message, boolean isImportanceLow) {
stopForeground(true);
int uniqueId = (int) System.currentTimeMillis();
Notification notification = getNotification(uniqueId, intent, message, isImportanceLow);
if (mNotificationManager != null) {
mNotificationManager.notify(uniqueId, notification);
Logger.d(LOG_TAG, "sendNotify()");
}
}
private Notification getNotification(int uniqueId, Intent intent, String message, boolean isImportanceLow) {
PendingIntent pendingIntent = PendingIntent.getActivity(this, uniqueId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
return new Notification.Builder(this)
.setDefaults(Notification.DEFAULT_ALL)
.setSmallIcon(R.drawable.icon)
.setWhen(System.currentTimeMillis())
.setAutoCancel(true)
.setContentTitle(getString(R.string.app_name))
.setContentText(message)
.setContentIntent(pendingIntent)
.setChannelId(isImportanceLow ? notificationChannelIdLow : notificationChannelId)
.build();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
......@@ -6,7 +6,9 @@ import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.widget.Toast;
......@@ -27,6 +29,10 @@ public class OnAppDownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return;
}
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
Logger.d("Download Complete ID : " + id);
......@@ -58,11 +64,25 @@ public class OnAppDownloadReceiver extends BroadcastReceiver {
if (downloadedTo != null && downloadedTo.toLowerCase().endsWith(".apk")) {
File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), ABVEnvironment.APK_FILE_NAME);
if (file.exists()) {
try {
// Android7でアップデート
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Uri apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file);
Intent updateIntent = new Intent(Intent.ACTION_VIEW);
updateIntent.setDataAndType(apkUri, "application/vnd.android.package-archive");
updateIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
updateIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
context.startActivity(updateIntent);
} else {
Intent i = new Intent(Intent.ACTION_VIEW);
// Activity以外からActivityを呼び出すためのフラグを設定
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
context.startActivity(i);
}
} catch (Exception ex) {
Logger.e("OnAppDownloadReceiver.startActivity(fileIntent).", ex);
}
} else {
Toast.makeText(context, "No Exist APK File: ", Toast.LENGTH_LONG).show();
}
......
package jp.agentec.abook.abv.ui.common.activity;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Color;
......@@ -70,7 +64,6 @@ import jp.agentec.abook.abv.cl.util.PreferenceUtil;
import jp.agentec.abook.abv.launcher.android.ABVApplication;
import jp.agentec.abook.abv.launcher.android.ABVUIDataCache;
import jp.agentec.abook.abv.launcher.android.R;
import jp.agentec.abook.abv.ui.common.appinfo.AppDefType.PrefName;
import jp.agentec.abook.abv.ui.common.appinfo.AppDefType.UserPrefKey;
import jp.agentec.abook.abv.ui.common.constant.ErrorCode;
import jp.agentec.abook.abv.ui.common.constant.ErrorMessage;
......@@ -256,7 +249,7 @@ public abstract class ABVActivity extends Activity {
}
if (progressDialog == null) {
progressDialog = new ProgressDialog(this);
progressDialog = new ProgressDialog(this, R.style.MyDialogTheme);
if (progressDialog.getWindow() != null) {
progressDialog.setMessage(getResources().getString(R.string.progress));
progressDialog.setIndeterminate(true);
......@@ -267,7 +260,7 @@ public abstract class ABVActivity extends Activity {
}
if (progressDialogHorizontal == null) {
progressDialogHorizontal = new ProgressDialog(this);
progressDialogHorizontal = new ProgressDialog(this, R.style.MyDialogTheme);
if (progressDialogHorizontal.getWindow() != null) {
progressDialogHorizontal.setIndeterminate(false);
progressDialogHorizontal.setMax(100); // 最大値を設定
......
......@@ -124,7 +124,7 @@ public abstract class ABVContentViewActivity extends ABVAuthenticatedActivity {
private String mReportFileName;
protected TextView operationNameTitle;
protected Button operationHomeButton;
protected ImageButton operationHomeButton;
protected ImageButton taskListButton;
// protected ImageButton helpButton;
......@@ -656,7 +656,7 @@ public abstract class ABVContentViewActivity extends ABVAuthenticatedActivity {
fl = (RelativeLayout) findViewById(R.id.frameTopbar);
}
fl.setBackgroundColor(getResources().getColor(R.color.operation_color));
operationHomeButton = (Button) findViewById(R.id.btn_operation_home);
operationHomeButton = (ImageButton) findViewById(R.id.btn_operation_home);
operationHomeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
......
......@@ -12,6 +12,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
......@@ -48,6 +49,7 @@ import jp.agentec.abook.abv.bl.logic.UserAuthenticateLogic;
import jp.agentec.abook.abv.cl.environment.NetworkAdapter;
import jp.agentec.abook.abv.cl.push.FcmManager;
import jp.agentec.abook.abv.cl.util.PreferenceUtil;
import jp.agentec.abook.abv.launcher.android.BackgroundDownloadService;
import jp.agentec.abook.abv.launcher.android.OnAppDownloadReceiver;
import jp.agentec.abook.abv.launcher.android.R;
import jp.agentec.abook.abv.ui.common.appinfo.AppDefType;
......@@ -255,7 +257,11 @@ public abstract class ABVNoAuthenticatedActivity extends ABVActivity {
public void onClick(DialogInterface dialog, int which) {
// バージョンアップフラグをON
PreferenceUtil.put(getApplicationContext(), AppDefType.DefPrefKey.APP_VERSIONUP_PROCESSING, true);
// Android8以上でバックグラウンドをフォアグラウンドで処理
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent serviceIntent = new Intent(mContext, BackgroundDownloadService.class);
startForegroundService(serviceIntent);
} else {
// Download 開始
DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
String currentDate = DateTimeUtil.toString(DateTimeUtil.getCurrentTimestamp(), DateTimeFormat.yyyyMMddHHmmss000_none);
......@@ -264,7 +270,7 @@ public abstract class ABVNoAuthenticatedActivity extends ABVActivity {
Request request = new Request(Uri.parse(downloadUrl));
request.setDescription("ABook Plus New Version File");
//LANケーブル接続のタイプ(ETHERNET TYPE)がないため、セットしない
// request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
// request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), ABVEnvironment.APK_FILE_NAME);
Logger.d(TAG, "download local file=%s", file.getAbsolutePath());
File[] childs = file.getParentFile().listFiles();
......@@ -275,6 +281,7 @@ public abstract class ABVNoAuthenticatedActivity extends ABVActivity {
downloadManager.enqueue(request);
registerReceiver(new OnAppDownloadReceiver(), new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
// アプリを閉じる
saveLeaveAppTime();
......
package jp.agentec.abook.abv.ui.home.activity;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Application;
import android.app.DownloadManager;
import android.app.DownloadManager.Request;
import android.content.Context;
......@@ -11,6 +13,7 @@ import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
......@@ -42,8 +45,11 @@ import jp.agentec.abook.abv.bl.logic.AbstractLogic;
import jp.agentec.abook.abv.bl.logic.ContractLogic;
import jp.agentec.abook.abv.bl.logic.UserAuthenticateLogic;
import jp.agentec.abook.abv.cl.helper.ABVUncaughtExceptionHandler;
import jp.agentec.abook.abv.cl.helper.PreferenceHelper;
import jp.agentec.abook.abv.cl.util.PreferenceUtil;
import jp.agentec.abook.abv.cl.util.RawResourceUtil;
import jp.agentec.abook.abv.launcher.android.ABVApplication;
import jp.agentec.abook.abv.launcher.android.BackgroundDownloadService;
import jp.agentec.abook.abv.launcher.android.OnAppDownloadReceiver;
import jp.agentec.abook.abv.launcher.android.R;
import jp.agentec.abook.abv.ui.common.appinfo.AppDefType;
......@@ -362,6 +368,12 @@ public class ABookSettingFragment extends PreferenceFragment {
dialog.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Android8以上でバックグラウンドをフォアグラウンドで処理
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Context context = getActivity().getApplicationContext();
Intent serviceIntent = new Intent(context, BackgroundDownloadService.class);
context.startForegroundService(serviceIntent);
} else {
// Download 開始
DownloadManager downloadManager = (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
String currentDate = DateTimeUtil.toString(DateTimeUtil.getCurrentTimestamp(), DateTimeFormat.yyyyMMddHHmmss000_none);
......@@ -370,7 +382,7 @@ public class ABookSettingFragment extends PreferenceFragment {
Request request = new Request(Uri.parse(downloadUrl));
request.setDescription("ABook Plus New Version File");
//LANケーブル接続のタイプ(ETHERNET TYPE)がないため、セットしない
// request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
// request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
File file = new File(getActivity().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), ABVEnvironment.APK_FILE_NAME);
Logger.d(TAG, "download local file=%s", file.getAbsolutePath());
......@@ -384,7 +396,7 @@ public class ABookSettingFragment extends PreferenceFragment {
}
getActivity().registerReceiver(new OnAppDownloadReceiver(), new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
// アプリを閉じる
saveLeaveAppTime();
getActivity().moveTaskToBack(true);
......
......@@ -25,7 +25,6 @@ import jp.agentec.abook.abv.bl.common.exception.NetworkDisconnectedException;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.ui.common.appinfo.AppDefType;
import jp.agentec.abook.abv.ui.common.util.PatternStringUtil;
import jp.agentec.abook.abv.ui.home.helper.ActivityHandlingHelper;
import jp.agentec.abook.abv.ui.home.helper.OzdFileHelper;
import jp.agentec.abook.abv.launcher.android.PDFFileProvider;
import jp.agentec.abook.abv.launcher.android.R;
......@@ -42,7 +41,6 @@ import oz.api.OZReportCommandListener;
import oz.api.OZReportViewer;
import static jp.agentec.abook.abv.cl.util.PreferenceUtil.getUserPref;
import static org.chromium.base.ContextUtils.getApplicationContext;
/**
* ABook Report(仮)ビュアー
......@@ -182,7 +180,7 @@ public class CheckOZDViewActivity extends ABVContentViewActivity {
@Override
public void onClick(View v) {
mButtonStatus = R.id.btn_close; // HTML側の分岐処理を行うため変数に値を渡す
//#41596 Zの報告入力フォームで☓をタップした際の動作がおかしい
//#41596 OZの報告入力フォームで☓をタップした際の動作がおかしい
//Javascript呼ばないようにし、連続作業と関係なく閉じる処理追加
finishActivity();
// doProcess(); // HTML側の処理を行う
......@@ -248,7 +246,17 @@ public class CheckOZDViewActivity extends ABVContentViewActivity {
Logger.i(TAG,"********mOzFilePath = %s" + mOzFilePath);
if (mOzFilePath == null) {
ABVToastUtil.showMakeText(this, "ozFilePath null", Toast.LENGTH_LONG);
// 帳票ファイルが存在しない場合は、アラートを表示して前の画面に戻る
ABookAlertDialog alertDialog = AlertDialogUtil.createAlertDialog(this, getResources().getString(R.string.error));
alertDialog.setMessage(getResources().getString(R.string.msg_ozd_file_could_not_opened));
alertDialog.setButton(DialogInterface.BUTTON_POSITIVE, getResources().getString(R.string.ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ozdCancelProcess(); // 閉じる
finishActivity();
}
});
showAlertDialog(alertDialog);
return;
}
......
......@@ -564,6 +564,15 @@ public class ContentViewActivity extends ABVContentViewActivity {
Logger.d(TAG, "[onConfigurationChanged]:newConfig=" + newConfig);
mShowPageLayout.clear();
isVideoMax = false;
//埋め込み動画を停止する。
stopVideo();
//全画面動画を停止する。
if (fullVideoView != null) {
fullVideoView.close();
fullVideoView = null;
}
//noinspection VariableNotUsedInsideIf
if (mContentWrapLayout != null) {
removeViews();
......@@ -2820,7 +2829,52 @@ public class ContentViewActivity extends ABVContentViewActivity {
meetingManager.sendWs(MeetingManager.CMD_MOVEPAGE, getContentId(), jumpPage, null, null);
}
}
/**
* ページを削除・作成する(ハイライト表示のため、全てのページ再作成)
* 検索結果一覧からのページ選択時&ハイライトリセット時利用
* @param jumpPage ジャンプページNO
*/
private void addOrRemoveSearchPages(final int jumpPage) {
final int currentPage = mCurrentPageNumber;
mCurrentPageNumber = jumpPage;
//作成済みの現在&前後ページの場合、そのページ削除
if (Math.abs(jumpPage - currentPage) < 2) {
removePage(jumpPage);
}
//ジャンプページ作成
addPageView(jumpPage);
new Handler().postDelayed(new Runnable() {
@Override
public void run() { // ジャンプするページに応じて必要なだけ作成・削除する
//Jump以前の後ページ情報削除
if (currentPage != jumpPage) {
removePage(currentPage);
}
//Jump以前の後ページ情報削除
int nextPage = currentPage + 1;
if (jumpPage != nextPage) {
removePage(nextPage);
}
//Jump以前の前ページ情報削除
int prevPage = currentPage - 1;
if (jumpPage != prevPage) {
removePage(prevPage);
}
//jumpした前後ページ情報作成
addPageView(jumpPage + 1);
addPageView(jumpPage - 1);
}
}, 500);
}
/**
* ページを削除・作成する(現在・前後ページはしない)
* 検索結果以外のところからページジャンプ時利用
* @param jumpPage ジャンプページNO
*/
private void addOrRemovePages(final int jumpPage) {
final int currentPage = mCurrentPageNumber;
mCurrentPageNumber = jumpPage;
......@@ -2856,7 +2910,7 @@ public class ContentViewActivity extends ABVContentViewActivity {
// ページ別ログ
ContentLogUtil.getInstance().contentPageMove(contentId, readingLogId, mCurrentPageNumber, jumpPage);
addOrRemovePages(jumpPage);
addOrRemoveSearchPages(jumpPage);
mPageScrollView.setZoomingFlag(false);
mPageScrollView.post(new Runnable() {
@Override
......@@ -3693,6 +3747,8 @@ public class ContentViewActivity extends ABVContentViewActivity {
for (int i = 0; i < imagefile.size(); i++) {
intent_.putExtra("FILEPATH" + (i + 1), mContentDir + "/" + imagefile.get(i));
}
intent_.putExtra("imageSize", imagefile.size());
intent_.putExtra("Position", 0);
intent_.putExtra(ABookKeys.CONTENT_ID, getContentId());
intent_.putExtra("pageNumber", mCurrentPageNumber);
......
......@@ -291,6 +291,15 @@ public class HTMLWebViewActivity extends ParentWebViewActivity {
}
return true;
}
//mailtoスキームチェック
String mailUrl = ABookKeys.MAIL_TO_URL;
if (url.length() > mailUrl.length() && mailUrl.equals(url.substring(0, mailUrl.length()))) {
//メーラー起動
Intent intent = new Intent(Intent.ACTION_SENDTO,Uri.parse(url));
startActivity(intent);
return true;
}
return false;
}
......
......@@ -352,6 +352,14 @@ public class HTMLXWalkWebViewActivity extends ParentWebViewActivity {
} else if (url.startsWith(ABookKeys.PING)) {
return true;
}
//mailtoスキームチェック
String mailUrl = ABookKeys.MAIL_TO_URL;
if (url.length() > mailUrl.length() && mailUrl.equals(url.substring(0, mailUrl.length()))) {
//メーラー起動
Intent intent = new Intent(Intent.ACTION_SENDTO,Uri.parse(url));
startActivity(intent);
return true;
}
return false;
}
});
......
......@@ -35,7 +35,7 @@ public class OnlineHTMLWebViewActivity extends ABVContentViewActivity {
private static final String TAG = "OnlineHTMLWebView";
private WebView webView;
private Button closeButton;
private ImageButton closeButton;
private ImageButton backButton;
@Override
......
......@@ -170,7 +170,7 @@ public class ParentWebViewActivity extends ABVContentViewActivity {
addSceneButton = (ImageButton) findViewById(R.id.btn_add_scene);
taskListButton = (ImageButton) findViewById(R.id.btn_show_task_list);
// helpButton = (ImageButton) findViewById(R.id.btn_help);
operationHomeButton = (Button) findViewById(R.id.btn_operation_home);
operationHomeButton = (ImageButton) findViewById(R.id.btn_operation_home);
if (mXWalkOpenType == Constant.XWalkOpenType.DEFAULT) {
// helpButton.setVisibility(View.GONE);
......
......@@ -8,6 +8,7 @@ import jp.agentec.abook.abv.cl.util.ContentLogUtil;
import jp.agentec.abook.abv.launcher.android.R;
import jp.agentec.abook.abv.ui.common.activity.ABVContentViewActivity;
import jp.agentec.abook.abv.ui.common.util.ABVToastUtil;
import jp.agentec.abook.abv.ui.common.util.DisplayUtil;
import jp.agentec.abook.abv.ui.home.helper.ActivityHandlingHelper;
import jp.agentec.abook.abv.ui.viewer.view.ActionZoomLayout;
......@@ -16,7 +17,8 @@ import org.json.adf.JSONObject;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.net.Uri;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.os.Bundle;
import android.view.Gravity;
import android.view.KeyEvent;
......@@ -47,6 +49,10 @@ public class PreviewActivity extends ABVContentViewActivity {
private int mHistoryImageIndex[] = new int[2];
private int objectLogId;
// 実際に描画するBitmap
// Androidの仕様で100MB以上のBitmapは扱えないため、アスペクト比を保存した状態でリサイズしたもの)
private Bitmap FripperBitmap[];
@Override
public void onCreate(Bundle savedInstanceState) {
Logger.i(TAG, "onCreate");
......@@ -61,7 +67,6 @@ public class PreviewActivity extends ABVContentViewActivity {
mZoomLayout.addView(getLayoutInflater().inflate(R.layout.ac_preview, null), paramMain);
setContentView(mZoomLayout);
mMaxImg = 0;
// 画像数の取得
Intent intent = getIntent();
if (objectId != -1 || ABVEnvironment.getInstance().disableLogSend) {
......@@ -70,14 +75,7 @@ public class PreviewActivity extends ABVContentViewActivity {
}
objectLogId = intent.getIntExtra("objectLogId", -1);
mPosition = intent.getIntExtra("Position", 0);
for (int i = 0; i < 8; i++) {
if (intent.getStringExtra("FILEPATH" + (i + 1)) == null) {
break;
} else {
mMaxImg++;
}
}
mMaxImg = intent.getIntExtra("imageSize", 0);
mViewFlipper = (ViewFlipper) findViewById(R.id.viewFlipperPreview);
mLayoutThumbnail = (LinearLayout)findViewById(R.id.layoutThumbnail);
......@@ -96,6 +94,7 @@ public class PreviewActivity extends ABVContentViewActivity {
finishActivity();
return;
} else {
// 画面下の画像ボタン作成
float tmpDensity = getResources().getDisplayMetrics().density;
Bitmap resized = BitmapUtil.getResizedBitmap(mFilePath[i], (int)(50 * tmpDensity + 0.5f), (int)(50 * tmpDensity + 0.5f), Config.RGB_565, false);
imgBtn[i].setImageBitmap(resized);
......@@ -109,10 +108,13 @@ public class PreviewActivity extends ABVContentViewActivity {
mLayoutThumbnail.addView(imgBtn[i], paramThumbnail);
setBtnClick(imgBtn[i], i);
}
// 数が多い可能性もあるので、最初の1枚目のみBitmap作成してViewに追加する
ImageView flipperImageView = new ImageView(this);
flipperImageView.setScaleType(ScaleType.CENTER_INSIDE);
flipperImageView.setAdjustViewBounds(true);
flipperImageView.setImageURI(Uri.parse(mFilePath[0]));
FripperBitmap = new Bitmap[mMaxImg];
FripperBitmap[0] = resizeBitmap(mFilePath[0]);
flipperImageView.setImageBitmap(FripperBitmap[0]);
LinearLayout.LayoutParams param = new LinearLayout.LayoutParams(wrapContent, wrapContent);
mFlipperLayout[0].addView(flipperImageView, param);
mHistoryImageIndex[0] = 1;
......@@ -228,7 +230,11 @@ public class PreviewActivity extends ABVContentViewActivity {
ImageView flipperImageView = new ImageView(this);
flipperImageView.setScaleType(ScaleType.FIT_CENTER); //CENTER_INSIDE
flipperImageView.setAdjustViewBounds(true);
flipperImageView.setImageURI(Uri.parse(mFilePath[index]));
if (FripperBitmap[index] == null) {
// 表示用Bitmapがない時は作成。
FripperBitmap[index] = resizeBitmap(mFilePath[index]);
}
flipperImageView.setImageBitmap(FripperBitmap[index]);
int MP = ViewGroup.LayoutParams.MATCH_PARENT;
LinearLayout.LayoutParams param = new LinearLayout.LayoutParams(MP, MP);
mFlipperLayout[index].removeAllViews();
......@@ -318,4 +324,42 @@ public class PreviewActivity extends ABVContentViewActivity {
super.onStop();
ContentLogUtil.getInstance().endObjectLog(getContentId(), objectLogId);
}
/**
* アスペクト比を維持して、横に引き延ばしたBitmapを作成する
* @param filePath ファイルパス
* @return リサイズしたBitmap
*/
private Bitmap resizeBitmap(String filePath) {
// ディスプレイ情報取得
Point point = DisplayUtil.getDisplaySize(this);
int dispWidth = point.x;
int dispHeight = point.y;
// Bitmap情報取得
BitmapFactory.Options options = BitmapUtil.getBitmapDimensions(filePath);
int bitmapWidth = options.outWidth;
int bitmpaHeight = options.outHeight;
// 変更するサイズを計算
int targetW;
int targetH;
if (bitmapWidth > bitmpaHeight) {
targetW = dispWidth;
targetH = (int) ((float) bitmpaHeight * (float) dispWidth / (float) bitmapWidth);
if (targetH > dispHeight) {
targetH = dispHeight;
targetW = (int) ((float) bitmapWidth * (float) dispHeight / (float) bitmpaHeight);
}
} else {
targetH = dispHeight;
targetW = (int) ((float) bitmapWidth * (float) dispHeight / (float) bitmpaHeight);
if (targetW > dispWidth) {
targetW = dispWidth;
targetH = (int) ((float) bitmpaHeight * (float) dispWidth / (float) bitmapWidth);
}
}
return BitmapUtil.getResizedBitmap(filePath, targetW, targetH, Config.RGB_565, true);
}
}
\ No newline at end of file
......@@ -154,6 +154,9 @@ public class EnqueteLayout extends RelativeLayout {
return false;
}
});
if (Build.VERSION.SDK_INT >= 29) {
mWebView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
mWebView.loadUrl(htmlPath);
}
......
......@@ -5,6 +5,7 @@ import android.content.Context;
import android.graphics.Color;
import android.graphics.PointF;
import android.net.Uri;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
......@@ -58,7 +59,7 @@ public class OperationTaskLayout extends RelativeLayout {
private static final int FINISHED_STATUS = 999;
private WebView mWebView = null;
private EnqueteWebView mWebView = null;
public ZoomRelativeLayout currentLayout;
public OperationTaskDto currentTaskDto;
......@@ -100,6 +101,10 @@ public class OperationTaskLayout extends RelativeLayout {
// mWebView.loadDataWithBaseURL("", url2, "text/html", "UTF-8", "");
settings.setAllowFileAccessFromFileURLs(true); //Android7利用で警告ダイヤログ表示問題対応
mWebView.setAlpha((int) (255 * 1.0f));
if (Build.VERSION.SDK_INT >= 29) {
mWebView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
mWebView.loadUrl(linkUrl);
RelativeLayout.LayoutParams params;
......
......@@ -844,19 +844,6 @@ public class ZoomRelativeLayout extends RelativeLayout {
//if (!mScaleDetector.isInProgress() && pageScrollView.isScrollable()) {
//스크롤 픽스 상태였을 경우 메모 추가가 안 되어서
if (!mScaleDetector.isInProgress() && mPageScrollView.isMemocheck()) {
// MemoAction
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child instanceof Action3DImageView) {
Action3DImageView a3dv = (Action3DImageView)child;
if (a3dv.getEventFlg() == true) {
// 3Dモードの場合は長押しメモ処理を行わない
return;
}
}
}
// PDF画面外側をタッチしているかチェック
mPdfScale = Math.min(getWidth() / (float)mPdfSize.width, getHeight() / (float)mPdfSize.height);
float marginX = getMarginX(mPdfScale);
......@@ -868,8 +855,27 @@ public class ZoomRelativeLayout extends RelativeLayout {
return;
}
if (mContentDto != null) {
//Android10以上ではAction3DImageViewの長押しイベントより、ZoomRelativeLayoutの長押しイベントが先に呼ばれる問題対応
//Androidバージョンチェックせずに全部対象とする。(0.3秒後にメモ表示するように修正)
getHandler().postDelayed(new Runnable() {
@Override
public void run() {
//3DView表示領域チェック
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child instanceof Action3DImageView) {
Action3DImageView a3dv = (Action3DImageView)child;
if (a3dv.getEventFlg() == true) {
// 3Dモードの場合は長押しメモ処理を行わない
Logger.d(TAG, "a3dv.getEventFlg() == true");
return;
}
}
}
showMemoMenu(x, y);
}
}, 300);
}
}
super.onLongPress(e);
......
package jp.agentec.abook.abv.ui.viewer.view.action;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.cl.util.BitmapUtil;
import jp.agentec.abook.abv.launcher.android.R;
import jp.agentec.abook.abv.ui.common.util.DisplayUtil;
import jp.agentec.abook.abv.ui.viewer.view.ActionImageView;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
......@@ -99,10 +102,14 @@ public class ImageChangeAction {
//noinspection deprecation(API16から非推奨になった。無視)
imgView.setBackgroundDrawable(null); //半透明表示
Bitmap myBitmap = BitmapFactory.decodeFile(imgfile);
imgView.setImageBitmap(myBitmap);
// イメージをアスペクト比を無視してリサイズしているため、処理をコメントアウトする
// Bitmap resized = BitmapUtil.getResizedBitmap(imgfile, w, h, Config.RGB_565, false); //イメージサイズをリサイズする
// imgView.setImageBitmap(resized);
// ディスプレイ情報取得して、アクセプト比を維持してリサイズ
Point point = DisplayUtil.getDisplaySize(context);
int dispWidth = point.x;
int dispHeight = point.y;
Bitmap resized = BitmapUtil.getResizedBitmap(imgfile,dispWidth,dispHeight);
imgView.setImageBitmap(resized);
layout.addView(imgView, param1);
return imgView;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment